[iOS翻译]如何在没有Xcode的情况下编写iOS应用程序

758 阅读34分钟

本文由 简悦SimpRead 转码,原文地址 betterprogramming.pub

你知道你有iOS的IDE选项吗?

你知道你有iOS的IDE选项吗?

image.png (没有Xcode IDE的iOS开发 (图片来源:作者))

Xcode是苹果公司的一个IDE(集成开发环境),有着悠久的历史。它是iOS开发的原生工具,同时支持Objective-C和Swift,包括XIB和Storyboard编辑器、编译器、调试器和开发所需的一切。

为什么会有人想在没有Xcode的情况下开发iOS应用呢?有几个可能的原因。

  • Xcode消耗大量的内存,在许多Mac上工作缓慢。
  • Xcode有很多bug。苹果公司修复了它们,但在新的功能中增加了新的错误。
  • Xcode只在macOS下工作,这让来自其他平台的开发者感到不舒服。

不幸的是,苹果尽一切可能阻止开发者使用其他平台;iOS模拟器是Xcode应用包的一部分,没有其他的方法来编辑Storyboards,而且在没有Xcode的情况下,要做一个完整的签名构建是非常复杂的。

所以,只是为了明确,有些事情是不可能的,所以不要期望克服这些限制。

  • 原生iOS应用程序只能在Mac上开发。你甚至可以在Windows或Linux上写代码,但你不能在那里构建和签署它。
  • 非原生平台,如Flutter或React Native,没有Mac也不会进行iOS构建。
  • 故事板只能在Xcode中编辑,所以没有Xcode的开发意味着没有故事板的开发。
  • 用于iOS开发的其他IDE需要Xcode。你不需要运行它,但你应该安装它。
  • 签署和上传应用程序到App Store(或Test Flight)可以通过命令行完成(见下文),但你需要安装Xcode。

我并不是说Xcode绝对不能用。恰恰相反,它是制作iOS应用程序最简单的方法。但是,如果你面临上面提到的困难之一,或者只是想尝试新的东西,这个故事就适合你。

原生或非原生?

iOS的应用程序可以是原生或非原生的。实际上,这并不是非黑即白,两者之间有很多选择。

绝对的原生应用是用Objective-C或Swift编写的。它们的某些部分可以用C或C++编写。用户界面通常以Storyboard或XIB文件呈现。

绝对的非原生应用只是用WKWebViewUIWebView)包装的网站。现在,苹果拒绝这样的应用程序,但在过去,它们是相当普遍的。很明显,这样的应用程序不需要与Xcode有太多的互动,用一个WebView创建一个UIViewController几乎不是问题,即使你在网上租用Mac,没有任何Xcode的经验。

所有其他选项都处于中间位置。它们使用本地组件,但代码通常是用非本地语言编写的。例如,Flutter使用Dart,React Native - JavaScript,Unity - C#。所有这些框架都使用自己的开发环境,但它们都输出一个Xcode项目。你需要用......Xcode来构建它来发布版本。通常情况下,这不是一个问题。文档中包含了为从未见过Xcode的人提供的分步说明。

在Android Studio中编写Flutter应用程序并不是这个故事的真正主题。它是一个默认选项,所以我们不会在上面浪费时间。我们将讨论在没有Xcode的情况下开发、测试和发布原生iOS应用程序。

我们有哪些问题?

让我们看看为什么在没有Xcode的情况下编写iOS应用程序并不那么容易。

  • 创建一个项目 - Xcode将你的工作保存在项目和工作空间中。工作区只是一组相互关联的项目。它们都是文件夹,里面有文本文件。这些文件的格式是专有的,但是它们很大并且有很多生成的id,所以它们不应该被人类编辑。
  • 用户接口 - 故事板是另一种专有格式。你不能在Xcode之外编辑它。
  • 构建和测试 - 有命令行编译器swiftcgccllvm,但如何制作一个可运行的iOS应用?

iOS工程

任何比 "Hello world "更复杂的应用程序都有不止一个文件。在iOS的情况下,即使是 "Hello world "也有不止一个文件。

在模拟器中可运行的iOS应用至少有两个组件。

  • 可执行的二进制文件
  • Info.plist文件

可在真实的iOS设备上运行的完整应用程序有更多的组件,但我们将在后面讨论这个问题。

要创建一个二进制文件,你至少需要两个项目。

  • 源代码
  • 构建脚本

iOS应用程序可以通过命令行来构建;例如,使用make。我们将在后面看如何做。

构建iOS应用程序的一个更舒适的方法是使用Xcode项目。我发现只有一个应用程序(除了Xcode)可以创建这样的项目--AppCode。它是JetBrain的应用程序之一,类似于IDEA和Android Studio,但用于苹果特定的开发,macOS和iOS。它只在macOS上运行,而且是付费的(从8.9美元/月或2020年4月的89美元/年起)。

警告! AppCode不能编辑Storyboards。当你尝试时,它会打开Xcode。而且它需要安装Xcode;否则,它将不会构建应用程序。

Xcode项目的结构

正如我前面提到的,Xcode项目不应该被手动编辑。

xcodeproj是一个包含一个文件和几个文件夹的文件夹。

这个文件是project.pbxproj。它包含了关于项目的所有信息,而且是最关键的文件。

警告! 如果你要编辑一个现有的项目,请做一个备份。

project.pbxproj是一个plist类文件。这种格式来自NeXTSTEP平台。现代的plist是XML,但project.pbxproj更类似于JSON(尽管它不是JSON)。

project.pbxproj主要是一组对象。每个对象都有一个唯一的标识符(一个96位的数字或24个十六进制字符的字符串)。对象可以是源文件、链接框架、构建阶段等。对象可以是一个组,包含其他对象。对象可以有属性和类型。其中一个对象是一个根对象。它的类型是 PBXProject。

如果在所有的警告之后,你决定编辑project.pbxproj文件,你可以使用Visual Studio Code的Syntax Xcode Project Data扩展。在这里你可以找到一个文件格式的详细描述

除了project.pbxproj,Xcode项目还包含几个文件夹。所有这些都是选项。

project.xcworkspace是一个只包含一个项目的工作区。当你在Xcode中打开一个项目文件时,它会自动创建,并包含构建模式、断点信息和其他数据,这不是项目的一部分。

xcuserdata是一个包含不同用户的个人数据的文件夹。如果你是唯一的开发者,里面将只有一个文件夹。这个文件夹是可选的,可以从Git和其他存储库中排除。

xcshareddata是一个包含用户间共享数据的文件夹;例如,方案。

如果你不使用Xcode,你只需要project.pbxproj

用make从控制台构建iOS应用程序

说实话,我觉得这样做项目太让人头疼了。用Xcode解决你的分歧(反正你需要安装它来签署应用程序)比手动做这么多步骤更容易。但是理论上的可能性是很有趣的,所以让我们深入挖掘一下。

首先,让我们得到一个SDK路径。

xcrun --sdk iphonesimulator --show-sdk-path

结果会是这样的。

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.4.sdk

Makefile里面,你可以粘贴xcrun的输出,或者使用该命令作为脚本的一部分。

SDKROOT:=$(shell xcrun --sdk iphonesimulator --show-sdk-path)

让我们制作一个 "应用程序 "目标。

app: main.m
    clang -isysroot $(SDKROOT) -framework Foundation -framework UIKit -o MakeTest.app/$@ $^

clang是一个来自Xcode软件包的标准编译器.

我们添加了两个框架。

  • Foundation
  • UIKit

输出文件是MakeTest.app/app

$@是一个自动变量,它被评估为一个目标的名称。在我们的例子中,它是app$^是另一个自动变量。它被评估为一个完整的依赖列表--在这个实验中是main.m

还有clean目标。

.PHONY: clean
clean:
    rm MakeTest.app/app

最后,让我们声明app是一个主要目标。

default: app

这是一个完整的Makefile

如果你从来没有使用过make来构建项目的话。

  • make app构建一个app目标。
  • make clean清理(删除app文件)。

由于我们声明了一个默认目标,我们可以直接使用make命令来构建项目。

下一步是创建一个app文件夹。是的,iOS应用程序是一个文件夹。

mkdir MakeTest.app

MakeTest.app中,应该有两个文件。

  • app是一个二进制文件,我们用make命令建立它。
  • Info.plist(以大写的 I 开头)是一个项目的属性列表。iOS设备或模拟器需要知道运行哪个二进制文件,它有哪个版本,以及其他数据。

这里是我们的Info.plist。如果你运行自己的测试,你可以改变一些字段。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleDisplayName</key>
	<string>MakeTest</string>
	<key>CFBundleExecutable</key>
	<string>app</string>
	<key>CFBundleIdentifier</key>
	<string>com.test.make</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>MakeTest</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0.0</string>
	<key>CFBundleSignature</key>
	<string>MAKE</string>
	<key>CFBundleVersion</key>
	<string>1</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
</dict>
</plist>

最后一个文件是main.m。在一个传统的iOS项目中,有三个不同的文件。

  • main.m
  • AppDelegate.h
  • AppDelegate.m

这只是一个组织问题,不是一个严格的规则。由于所有的组件都非常小,让我们把它们放在一起。

这里是应用程序的主要功能。它的唯一目的是进入主循环。我们还传递了一个应用程序委托的名称 - AppDelegate

int main(int argc, char *argv[]) {
  @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

如果你用Xcode或AppCode创建一个项目,这个函数将自动生成。

不要忘记把UIKit包含在你项目的所有Objective-C文件中。

#`import <UIKit/UIKit.h>

如果你来自Swift,你可能不知道在Objective-C中(和C++一样)每个类都必须被声明和定义。

类的声明。

@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

类的定义。

@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(id)options {
  CGRect mainScreenBounds = [[UIScreen mainScreen] bounds];
  self.window = [[UIWindow alloc] initWithFrame:mainScreenBounds];
  
  UIViewController *viewController = [[UIViewController alloc] init];
  viewController.view.backgroundColor = [UIColor whiteColor];
  viewController.view.frame = mainScreenBounds;
  UILabel *label = [[UILabel alloc] initWithFrame:mainScreenBounds];
  [label setText:@"Wow! I was built with clang and make!"];
  [viewController.view addSubview: label];
  self.window.rootViewController = viewController;
  [self.window makeKeyAndVisible];
  
  return YES;
}
@end

它创建了一个窗口、视图控制器,并在其中心创建了一个标签。我就不多说了,因为这与Xcode之外的编码没有关系。这只是iOS编程。

应用程序委托、视图控制器和其他组件可以放在单独的文件中。更有甚者,这也是一种推荐的做法。从技术上讲,一个基于Makefile的项目可以使用与普通Xcode项目相同的结构。

下面是它的样子。

image.png

不用Xcode构建的iOS项目,在iPhone 11模拟器中运行。

main.m的全部源代码。

那么,接下来是什么?

  • 权限文件 - 如果你需要在你的iOS项目中添加任何功能,你需要一个权限文件。这是一个带有键值对的plist文件,描述了它使用的iOS设备或苹果用户账户的哪些资源或服务。你可以在苹果文档中找到更多细节。
  • 你可以看到,我们的例子中的应用程序并没有采用全屏显示。这可以通过添加启动图片或适当的尺寸,或启动屏幕故事板来解决。
  • 制作两个目标:用于iOS设备和模拟器。iOS构建需要签名,我们稍后再谈。
  • 应用图标可以作为assets.xcassets文件夹的一部分添加,或者作为一组PNG文件(在Info.plist中引用)。Assets.xcassets是一个包含应用程序资产的文件夹。我们以后会再来讨论它。

如果你有以这种方式构建商业iOS应用的经验,请留下评论。我将很高兴根据你的经验加入更多的信息。

我发现有四种方法可以在没有Xcode和Storyboards的情况下构建用户界面。

  • SwiftUI - 苹果公司的新UI构建器可以在任何文本编辑器中进行编辑。例如,Visual Studio Code(VS Code)有一个插件可以编写Swift代码。而且它是免费的。缺点是,你需要iOS 13才能用SwiftUI运行一个应用程序。几年后,这将不再是一个缺点,但现在放弃对iOS 12和早期版本的支持还为时过早。
  • 从代码中创建组件 - 任何组件都可以从Objective-C或Swift代码中创建,无需任何UI设计师。这很漫长,也很不舒服,但非常普遍。代码可以写在任何地方,你的应用程序甚至可以在第一代iPhone上运行(如果你设法为这样一个旧设备建立它)。
  • 外部工具 - 有一些工具可以将设计(例如在Sketch中制作的)转换为本地iOS代码(Objective-C或Swift)。这种工具的一个例子是Supernova。它是付费的,就像任何其他具有类似功能的工具一样。
  • 外部库 - 这些库允许你编写简短的代码来建立一个本地UI。它们是免费的,但你需要学习如何使用它们。这几乎就像学习一种新的编程语言。例如。LayoutKit 来自LinkedIn。

没有完美的解决方案。它们中的每一个都有优点和缺点,所以要看你用什么。我认为随着时间的推移,SwiftUI将成为为iOS构建UI的最流行方式。但如果你需要更快地发布你的应用程序,你最好使用另一种方式。在任何情况下,这都是你的选择。

SwiftUI

SwiftUI是苹果公司的一个新框架,应该是用来取代UIKit和Storyboards的。

优点。

  • 它是iOS的原生框架。苹果会支持它,推广它,并激励开发者使用它。
  • SwiftUI的代码可以在任何文本编辑器中编写。
  • SwiftUI可以与UIKit混合在同一个布局中。

缺点。

  • SwiftUI只能针对iOS 13或更高版本。它不会在iOS 12上运行。
  • 布局预览和模拟只在Xcode中工作。没有用于AppCode、VS Code或任何其他代码编辑器的插件。

让我们看看SwiftUI是如何工作的。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            MapView()
                .edgesIgnoringSafeArea(.top)
                .frame(height: 300)

            CircleImage()
                .offset(x: 0, y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack(alignment: .top) {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()

            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

这个例子借用了官方的苹果教程

在这里你可以找到另一个详细的SwiftUI教程

从代码中创建UIKit组件

这是一种绝对通用的创建布局的方式。你可以创建组件,添加约束,基本上做任何事情。但每个组件都需要完全初始化。而当你看不到你在做什么的时候,与约束条件一起工作就是一场噩梦了。只有在模拟器或iOS设备上才能预览布局,而且每次修正都需要重新启动应用程序。

这就是它的工作原理。

override func viewDidLoad()  {
    super.viewDidLoad()

    let continueButton = UIButton()
    continueButton.setTitle("Continue", for: .normal)
    continueButton.setTitleColor(UIColor.blue, for: .normal)
    continueButton.frame = CGRect(x: 16, y: 32, width: UIScreen.main.bounds.width - 32, height: 44)
    continueButton.addTarget(self, action: #selector(pressedButton(_:)), for: .touchUpInside)
    self.view.addSubview(continueButton)
}

@objc func pressedButton(_ button: UIButton) {
    // Do something
}

这段代码应该在UIViewController子类里面。

每个UI组件都应该这样创建。它们可以嵌套,就像在Storyboard中一样。故事板的所有功能和UIKit的所有组件都可以通过代码获得,包括表格、集合等。

外部工具

设计师使用Sketch、Adobe XD、Zeplin或其他工具进行iOS布局。如果你能直接把它们导出到iOS应用中,那不是很好吗?当然可以。这是有可能的,但不是免费的。

我找到了几个允许这样做的工具。

请注意,价格不是固定的,随时都可能改变。

这些工具/插件的结果是原生的iOS代码,Swift,或Objective-C。

比方说,我们有一个用Sketch做的设计。在这个例子中,我下载了Waste Management App Sketch Resource这个文件。

image.png (Sketch)

Sketch应用程序打开它时有一个警告(版本不匹配),但这不是问题。下一步--超新星。

image.png (Supernova Studio)

Supernova Studio有一个功能可以导入Sketch或Adobe XD文件。

image.png (导入Sketch到Supernova Studio)

导入成功了,但也有一些小毛病。比如说,后退的箭头(见截图)。另外,在预览时,文本与按钮重叠,但这应该用约束或UIScrollView来解决。

image.png (Supernova中的iOS屏幕设计)

箭头的问题可以通过使用栅格图片或手动来解决。让我们看看它是如何被导出到iOS代码的。文件导出到iOS。试图导出时,我看到了一个缺失字体的列表。

image.png (缺少的字体)

好吧,让我们暂时忽略它。系统字体是好的。

我只为iPhone选择了导出,而且只为纵向导出,为了不深究细节。

image.png (从Supernova导出)

语言只能是Swift。对于我来说,选择最新的才有意义。目前,它是Swift 5。UI必须是Code。其他选项是Storyboard和XIB,但它们只能由Xcode打开,所以对我们来说不是一个选项。

导出过程花了不到一分钟。它生成了一个带有Podfile的Xcode项目。Podfile几乎是空的;它只有一个骨架,但没有依赖性。

platform :ios, '11.0'
inhibit_all_warnings!

target 'Waste-management-app-musafarouk' do

  # Add all your pods here


  # Supernova Pods


end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    puts "#{target.name}"
  end
end

不管怎样,让我们安装Pods来生成一个工作区。

image.png (由Supernova Studio生成的项目)

每个屏幕都是一个独立的类,是UIViewController的子类,位于不同的文件夹中。字体、图像和其他资产也被导出。而且没有故事板。这意味着我们可以用AppCode打开它。

下面是一个Supernova Studio导出的例子,文件HomeActivityNavViewController

//
//  HomeActiveNavViewController.swift
//  Waste-management-app-musafarouk
//
//  Created by Supernova.
//  Copyright © 2018 Supernova. All rights reserved.
//
// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
// MARK: - Import
import UIKit


// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
// MARK: - Implementation
class HomeActiveNavViewController: UIViewController {


    // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
    // MARK: - Properties
    var homeActiveNavView: UIView!
    var snazzyImage5ImageView: UIImageView!
    var lineImageView: UIImageView!
    var rectangleView: UIView!
    var rectangleTwoView: UIView!
    var group6View: UIView!
    var acmeWasteDisposalLabel: UILabel!
    var group4View: UIView!
    var birninKebbiCreLabel: UILabel!
    var icLocationOn24pxImageView: UIImageView!
    var routingLabel: UILabel!
    var group5View: UIView!
    var stopLabel: UILabel!
    var targetView: UIView!
    var outlinedUiMapTargetImageView: UIImageView!
    var binView: UIView!
    var recyclingBinImageView: UIImageView!
    var locationView: UIView!
    var ovalView: UIView!
    var ovalTwoView: UIView!
    var ovalThreeView: UIView!
    var pathImageView: UIImageView!
    var ovalImageView: UIImageView!
    var rectangleThreeView: UIView!
    var group3View: UIView!
    var menuLeftImageView: UIImageView!
    var groupImageView: UIImageView!
    private var allGradientLayers: [CAGradientLayer] = []


    // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
    // MARK: - Lifecycle
    override public func viewDidLoad()  {
        super.viewDidLoad()
        self.setupComponents()
        self.setupLayout()
        self.setupUI()
        self.setupGestureRecognizers()
        self.setupLocalization()
        
        // Do any additional setup after loading the view, typically from a nib.
    }

    override public func viewWillAppear(_ animated: Bool)  {
        super.viewWillAppear(animated)
        
        // Navigation bar, if any
        self.navigationController?.setNavigationBarHidden(true, animated: true)
    }


    // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
    // MARK: - Setup
    private func setupComponents()  {
        // Setup homeActiveNavView
        self.view.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1) /* #FFFFFF */
        self.view.translatesAutoresizingMaskIntoConstraints = true
        
        // Setup snazzyImage5ImageView
        self.snazzyImage5ImageView = UIImageView()
        self.snazzyImage5ImageView.backgroundColor = UIColor.clear
        self.snazzyImage5ImageView.image = UIImage(named: "snazzy-image-5")
        self.snazzyImage5ImageView.contentMode = .scaleAspectFill
        self.view.addSubview(self.snazzyImage5ImageView)
        self.snazzyImage5ImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup lineImageView
        self.lineImageView = UIImageView()
        self.lineImageView.backgroundColor = UIColor.clear
        self.lineImageView.image = UIImage(named: "line")
        self.lineImageView.contentMode = .center
        self.view.addSubview(self.lineImageView)
        self.lineImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup rectangleView
        self.rectangleView = UIView(frame: .zero)
        let rectangleViewGradient = CAGradientLayer()
        rectangleViewGradient.colors = [UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).cgColor /* #000000 */, UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).cgColor /* #000000 */, UIColor.clear.cgColor]
        rectangleViewGradient.locations = [0, 0, 1]
        rectangleViewGradient.startPoint = CGPoint(x: 0.5, y: 1)
        rectangleViewGradient.endPoint = CGPoint(x: 0.5, y: 0)
        rectangleViewGradient.frame = self.rectangleView.bounds
        self.rectangleView.layer.insertSublayer(rectangleViewGradient, at: 0)
        self.allGradientLayers.append(rectangleViewGradient)
        
        self.view.addSubview(self.rectangleView)
        self.rectangleView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup rectangleTwoView
        self.rectangleTwoView = UIView(frame: .zero)
        self.rectangleTwoView.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2).cgColor /* #000000 */
        self.rectangleTwoView.layer.shadowOffset = CGSize(width: 0, height: 10)
        self.rectangleTwoView.layer.shadowRadius = 20
        self.rectangleTwoView.layer.shadowOpacity = 1
        
        self.rectangleTwoView.backgroundColor = UIColor(red: 0.2, green: 0.216, blue: 0.294, alpha: 1) /* #33374B */
        self.rectangleTwoView.layer.cornerRadius = 6
        self.rectangleTwoView.layer.masksToBounds = true
        self.view.addSubview(self.rectangleTwoView)
        self.rectangleTwoView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup group6View
        self.group6View = UIView(frame: .zero)
        self.group6View.backgroundColor = UIColor.clear
        self.view.addSubview(self.group6View)
        self.group6View.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup acmeWasteDisposalLabel
        self.acmeWasteDisposalLabel = UILabel()
        self.acmeWasteDisposalLabel.numberOfLines = 0
        let acmeWasteDisposalLabelAttrString = NSMutableAttributedString(string: "Acme waste disposal", attributes: [
            .font : UIFont.systemFont(ofSize: 20),
            .foregroundColor : UIColor(red: 1, green: 1, blue: 1, alpha: 1),
            .kern : 0.5,
            .paragraphStyle : NSMutableParagraphStyle(alignment: .left, lineHeight: 25, paragraphSpacing: 0)
        ])
        self.acmeWasteDisposalLabel.attributedText = acmeWasteDisposalLabelAttrString
        self.acmeWasteDisposalLabel.backgroundColor = UIColor.clear
        self.group6View.addSubview(self.acmeWasteDisposalLabel)
        self.acmeWasteDisposalLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup group4View
        self.group4View = UIView(frame: .zero)
        self.group4View.backgroundColor = UIColor.clear
        self.group6View.addSubview(self.group4View)
        self.group4View.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup birninKebbiCreLabel
        self.birninKebbiCreLabel = UILabel()
        self.birninKebbiCreLabel.numberOfLines = 0
        let birninKebbiCreLabelAttrString = NSMutableAttributedString(string: "25, Birnin Kebbi Cres, Garki, Abuja • 2km", attributes: [
            .font : UIFont.systemFont(ofSize: 12),
            .foregroundColor : UIColor(red: 1, green: 1, blue: 1, alpha: 1),
            .kern : 0.3,
            .paragraphStyle : NSMutableParagraphStyle(alignment: .left, lineHeight: 20, paragraphSpacing: 0)
        ])
        self.birninKebbiCreLabel.attributedText = birninKebbiCreLabelAttrString
        self.birninKebbiCreLabel.backgroundColor = UIColor.clear
        self.birninKebbiCreLabel.alpha = 0.5
        self.group4View.addSubview(self.birninKebbiCreLabel)
        self.birninKebbiCreLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup icLocationOn24pxImageView
        self.icLocationOn24pxImageView = UIImageView()
        self.icLocationOn24pxImageView.backgroundColor = UIColor.clear
        self.icLocationOn24pxImageView.image = UIImage(named: "ic-location-on-24px-2")
        self.icLocationOn24pxImageView.contentMode = .center
        self.group4View.addSubview(self.icLocationOn24pxImageView)
        self.icLocationOn24pxImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup routingLabel
        self.routingLabel = UILabel()
        self.routingLabel.numberOfLines = 0
        let routingLabelAttrString = NSMutableAttributedString(string: "Routing…", attributes: [
            .font : UIFont.systemFont(ofSize: 14),
            .foregroundColor : UIColor(red: 1, green: 1, blue: 1, alpha: 1),
            .kern : 0.44,
            .paragraphStyle : NSMutableParagraphStyle(alignment: .left, lineHeight: 25, paragraphSpacing: 0)
        ])
        self.routingLabel.attributedText = routingLabelAttrString
        self.routingLabel.backgroundColor = UIColor.clear
        self.routingLabel.alpha = 0.5
        self.view.addSubview(self.routingLabel)
        self.routingLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup group5View
        self.group5View = UIView(frame: .zero)
        self.group5View.backgroundColor = UIColor(red: 0.627, green: 0, blue: 0, alpha: 1) /* #A00000 */
        self.group5View.layer.cornerRadius = 4
        self.group5View.layer.masksToBounds = true
        self.view.addSubview(self.group5View)
        self.group5View.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup stopLabel
        self.stopLabel = UILabel()
        self.stopLabel.numberOfLines = 0
        let stopLabelAttrString = NSMutableAttributedString(string: "Stop", attributes: [
            .font : UIFont.systemFont(ofSize: 14),
            .foregroundColor : UIColor(red: 1, green: 1, blue: 1, alpha: 1),
            .kern : 0.44,
            .paragraphStyle : NSMutableParagraphStyle(alignment: .center, lineHeight: nil, paragraphSpacing: 0)
        ])
        self.stopLabel.attributedText = stopLabelAttrString
        self.stopLabel.backgroundColor = UIColor.clear
        self.group5View.addSubview(self.stopLabel)
        self.stopLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup targetView
        self.targetView = UIView(frame: .zero)
        self.targetView.layer.shadowColor = UIColor(red: 0.141, green: 0.149, blue: 0.2, alpha: 1).cgColor /* #242633 */
        self.targetView.layer.shadowOffset = CGSize(width: 0, height: 11)
        self.targetView.layer.shadowRadius = 23
        self.targetView.layer.shadowOpacity = 1
        
        self.targetView.backgroundColor = UIColor(red: 0.141, green: 0.149, blue: 0.2, alpha: 1) /* #242633 */
        self.targetView.layer.cornerRadius = 25
        self.targetView.layer.masksToBounds = true
        self.view.addSubview(self.targetView)
        self.targetView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup outlinedUiMapTargetImageView
        self.outlinedUiMapTargetImageView = UIImageView()
        self.outlinedUiMapTargetImageView.backgroundColor = UIColor.clear
        self.outlinedUiMapTargetImageView.image = UIImage(named: "outlined-ui-map-target")
        self.outlinedUiMapTargetImageView.contentMode = .center
        self.targetView.addSubview(self.outlinedUiMapTargetImageView)
        self.outlinedUiMapTargetImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup binView
        self.binView = UIView(frame: .zero)
        self.binView.layer.borderColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1).cgColor /* #FFFFFF */
        self.binView.layer.borderWidth = 5
        
        self.binView.backgroundColor = UIColor(red: 0.031, green: 0.451, blue: 0.239, alpha: 1) /* #08733D */
        self.binView.layer.cornerRadius = 10
        self.binView.layer.masksToBounds = true
        self.view.addSubview(self.binView)
        self.binView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup recyclingBinImageView
        self.recyclingBinImageView = UIImageView()
        self.recyclingBinImageView.backgroundColor = UIColor.clear
        self.recyclingBinImageView.image = UIImage(named: "recycling-bin")
        self.recyclingBinImageView.contentMode = .center
        self.binView.addSubview(self.recyclingBinImageView)
        self.recyclingBinImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup locationView
        self.locationView = UIView(frame: .zero)
        self.locationView.backgroundColor = UIColor.clear
        self.view.addSubview(self.locationView)
        self.locationView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup ovalView
        self.ovalView = UIView(frame: .zero)
        self.ovalView.backgroundColor = UIColor(red: 0.031, green: 0.451, blue: 0.239, alpha: 1) /* #08733D */
        self.ovalView.layer.cornerRadius = 49
        self.ovalView.layer.masksToBounds = true
        self.ovalView.alpha = 0.1
        self.locationView.addSubview(self.ovalView)
        self.ovalView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup ovalTwoView
        self.ovalTwoView = UIView(frame: .zero)
        self.ovalTwoView.backgroundColor = UIColor(red: 0.031, green: 0.451, blue: 0.239, alpha: 1) /* #08733D */
        self.ovalTwoView.layer.cornerRadius = 65
        self.ovalTwoView.layer.masksToBounds = true
        self.ovalTwoView.alpha = 0.05
        self.locationView.addSubview(self.ovalTwoView)
        self.ovalTwoView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup ovalThreeView
        self.ovalThreeView = UIView(frame: .zero)
        self.ovalThreeView.layer.shadowColor = UIColor(red: 0.031, green: 0.451, blue: 0.239, alpha: 1).cgColor /* #08733D */
        self.ovalThreeView.layer.shadowOffset = CGSize(width: 0, height: 5)
        self.ovalThreeView.layer.shadowRadius = 10
        self.ovalThreeView.layer.shadowOpacity = 1
        
        self.ovalThreeView.backgroundColor = UIColor(red: 0.031, green: 0.451, blue: 0.239, alpha: 1) /* #08733D */
        self.ovalThreeView.layer.cornerRadius = 16
        self.ovalThreeView.layer.masksToBounds = true
        self.locationView.addSubview(self.ovalThreeView)
        self.ovalThreeView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup pathImageView
        self.pathImageView = UIImageView()
        self.pathImageView.backgroundColor = UIColor.clear
        self.pathImageView.image = UIImage(named: "path")
        self.pathImageView.contentMode = .center
        self.locationView.addSubview(self.pathImageView)
        self.pathImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup ovalImageView
        self.ovalImageView = UIImageView()
        self.ovalImageView.backgroundColor = UIColor.clear
        self.ovalImageView.image = UIImage(named: "oval-5")
        self.ovalImageView.contentMode = .center
        self.view.addSubview(self.ovalImageView)
        self.ovalImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup rectangleThreeView
        self.rectangleThreeView = UIView(frame: .zero)
        let rectangleThreeViewGradient = CAGradientLayer()
        rectangleThreeViewGradient.colors = [UIColor(red: 0, green: 0, blue: 0, alpha: 1).cgColor /* #000000 */, UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).cgColor /* #000000 */, UIColor.clear.cgColor]
        rectangleThreeViewGradient.locations = [0, 0, 1]
        rectangleThreeViewGradient.startPoint = CGPoint(x: 0.5, y: 0)
        rectangleThreeViewGradient.endPoint = CGPoint(x: 0.5, y: 1)
        rectangleThreeViewGradient.frame = self.rectangleThreeView.bounds
        self.rectangleThreeView.layer.insertSublayer(rectangleThreeViewGradient, at: 0)
        self.allGradientLayers.append(rectangleThreeViewGradient)
        
        self.view.addSubview(self.rectangleThreeView)
        self.rectangleThreeView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup group3View
        self.group3View = UIView(frame: .zero)
        self.group3View.backgroundColor = UIColor.clear
        self.view.addSubview(self.group3View)
        self.group3View.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup menuLeftImageView
        self.menuLeftImageView = UIImageView()
        self.menuLeftImageView.backgroundColor = UIColor.clear
        self.menuLeftImageView.image = UIImage(named: "menu-left")
        self.menuLeftImageView.contentMode = .center
        self.group3View.addSubview(self.menuLeftImageView)
        self.menuLeftImageView.translatesAutoresizingMaskIntoConstraints = false
        
        // Setup groupImageView
        self.groupImageView = UIImageView()
        self.groupImageView.backgroundColor = UIColor.clear
        self.groupImageView.image = UIImage(named: "group-48")
        self.groupImageView.contentMode = .center
        self.group3View.addSubview(self.groupImageView)
        self.groupImageView.translatesAutoresizingMaskIntoConstraints = false
        
    }

    private func setupUI()  {
        self.extendedLayoutIncludesOpaqueBars = true
        
        self.navigationController?.setNavigationBarHidden(true, animated: true)
    }

    private func setupGestureRecognizers()  {
    
    }

    private func setupLocalization()  {
    
    }


    // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
    // MARK: - Layout
    override public func viewDidLayoutSubviews()  {
        super.viewDidLayoutSubviews()
        for layer in self.allGradientLayers {
            layer.frame = layer.superlayer?.frame ?? CGRect.zero
        }
    }

    private func setupLayout()  {
        // Setup layout for components
        // Setup homeActiveNavView
        
        // Setup snazzyImage5ImageView
        self.snazzyImage5ImageView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: -303).isActive = true
        self.snazzyImage5ImageView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 222).isActive = true
        self.snazzyImage5ImageView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
        
        // Setup lineImageView
        self.lineImageView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -98).isActive = true
        self.lineImageView.topAnchor.constraint(equalTo: self.rectangleThreeView.bottomAnchor, constant: 34).isActive = true
        
        // Setup rectangleView
        self.rectangleView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
        self.rectangleView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
        self.rectangleView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
        self.rectangleView.heightAnchor.constraint(equalToConstant: 90).isActive = true
        
        // Setup rectangleTwoView
        self.rectangleTwoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 17).isActive = true
        self.rectangleTwoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -18).isActive = true
        self.rectangleTwoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -35).isActive = true
        self.rectangleTwoView.heightAnchor.constraint(equalToConstant: 216).isActive = true
        
        // Setup group6View
        self.group6View.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 42).isActive = true
        self.group6View.bottomAnchor.constraint(equalTo: self.routingLabel.topAnchor, constant: -16).isActive = true
        self.group6View.widthAnchor.constraint(equalToConstant: 280).isActive = true
        self.group6View.heightAnchor.constraint(equalToConstant: 68).isActive = true
        
        // Setup acmeWasteDisposalLabel
        self.acmeWasteDisposalLabel.leadingAnchor.constraint(equalTo: self.group6View.leadingAnchor, constant: 0).isActive = true
        self.acmeWasteDisposalLabel.trailingAnchor.constraint(equalTo: self.group6View.trailingAnchor, constant: -87).isActive = true
        self.acmeWasteDisposalLabel.topAnchor.constraint(equalTo: self.group6View.topAnchor, constant: -1).isActive = true
        
        // Setup group4View
        self.group4View.leadingAnchor.constraint(equalTo: self.group6View.leadingAnchor, constant: 0).isActive = true
        self.group4View.topAnchor.constraint(equalTo: self.acmeWasteDisposalLabel.bottomAnchor, constant: 4).isActive = true
        self.group4View.widthAnchor.constraint(equalToConstant: 220).isActive = true
        self.group4View.heightAnchor.constraint(equalToConstant: 40).isActive = true
        
        // Setup birninKebbiCreLabel
        self.birninKebbiCreLabel.leadingAnchor.constraint(equalTo: self.icLocationOn24pxImageView.trailingAnchor, constant: 8).isActive = true
        self.birninKebbiCreLabel.trailingAnchor.constraint(equalTo: self.group4View.trailingAnchor, constant: 0).isActive = true
        self.birninKebbiCreLabel.topAnchor.constraint(equalTo: self.group4View.topAnchor, constant: -3).isActive = true
        self.birninKebbiCreLabel.widthAnchor.constraint(equalToConstant: 205).isActive = true
        
        // Setup icLocationOn24pxImageView
        self.icLocationOn24pxImageView.leadingAnchor.constraint(equalTo: self.group4View.leadingAnchor, constant: 0).isActive = true
        self.icLocationOn24pxImageView.topAnchor.constraint(equalTo: self.group4View.topAnchor, constant: 4).isActive = true
        
        // Setup routingLabel
        self.routingLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 42).isActive = true
        self.routingLabel.bottomAnchor.constraint(equalTo: self.group5View.topAnchor, constant: -30).isActive = true
        
        // Setup group5View
        self.group5View.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0).isActive = true
        self.group5View.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -51).isActive = true
        self.group5View.widthAnchor.constraint(equalToConstant: 290).isActive = true
        self.group5View.heightAnchor.constraint(equalToConstant: 45).isActive = true
        
        // Setup stopLabel
        self.stopLabel.centerXAnchor.constraint(equalTo: self.group5View.centerXAnchor, constant: 0).isActive = true
        self.stopLabel.centerYAnchor.constraint(equalTo: self.group5View.centerYAnchor, constant: 0).isActive = true
        
        // Setup targetView
        self.targetView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -18).isActive = true
        self.targetView.topAnchor.constraint(equalTo: self.rectangleThreeView.bottomAnchor, constant: 401).isActive = true
        self.targetView.widthAnchor.constraint(equalToConstant: 50).isActive = true
        self.targetView.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        // Setup outlinedUiMapTargetImageView
        self.outlinedUiMapTargetImageView.leadingAnchor.constraint(equalTo: self.targetView.leadingAnchor, constant: 12).isActive = true
        self.outlinedUiMapTargetImageView.trailingAnchor.constraint(equalTo: self.targetView.trailingAnchor, constant: -12).isActive = true
        self.outlinedUiMapTargetImageView.centerYAnchor.constraint(equalTo: self.targetView.centerYAnchor, constant: 0).isActive = true
        
        // Setup binView
        self.binView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -78).isActive = true
        self.binView.topAnchor.constraint(equalTo: self.rectangleThreeView.bottomAnchor, constant: 5).isActive = true
        self.binView.widthAnchor.constraint(equalToConstant: 50).isActive = true
        self.binView.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        // Setup recyclingBinImageView
        self.recyclingBinImageView.leadingAnchor.constraint(equalTo: self.binView.leadingAnchor, constant: 16).isActive = true
        self.recyclingBinImageView.trailingAnchor.constraint(equalTo: self.binView.trailingAnchor, constant: -16).isActive = true
        self.recyclingBinImageView.centerYAnchor.constraint(equalTo: self.binView.centerYAnchor, constant: 0).isActive = true
        
        // Setup locationView
        self.locationView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0).isActive = true
        self.locationView.topAnchor.constraint(equalTo: self.binView.bottomAnchor, constant: 204).isActive = true
        self.locationView.widthAnchor.constraint(equalToConstant: 130).isActive = true
        self.locationView.heightAnchor.constraint(equalToConstant: 130).isActive = true
        
        // Setup ovalView
        self.ovalView.leadingAnchor.constraint(equalTo: self.locationView.leadingAnchor, constant: 16).isActive = true
        self.ovalView.trailingAnchor.constraint(equalTo: self.locationView.trailingAnchor, constant: -16).isActive = true
        self.ovalView.centerYAnchor.constraint(equalTo: self.locationView.centerYAnchor, constant: 0).isActive = true
        self.ovalView.heightAnchor.constraint(equalToConstant: 98).isActive = true
        
        // Setup ovalTwoView
        self.ovalTwoView.leadingAnchor.constraint(equalTo: self.locationView.leadingAnchor, constant: 0).isActive = true
        self.ovalTwoView.trailingAnchor.constraint(equalTo: self.locationView.trailingAnchor, constant: 0).isActive = true
        self.ovalTwoView.centerYAnchor.constraint(equalTo: self.locationView.centerYAnchor, constant: 0).isActive = true
        self.ovalTwoView.heightAnchor.constraint(equalToConstant: 130).isActive = true
        
        // Setup ovalThreeView
        self.ovalThreeView.centerXAnchor.constraint(equalTo: self.locationView.centerXAnchor, constant: 0).isActive = true
        self.ovalThreeView.centerYAnchor.constraint(equalTo: self.locationView.centerYAnchor, constant: 0).isActive = true
        self.ovalThreeView.widthAnchor.constraint(equalToConstant: 32).isActive = true
        self.ovalThreeView.heightAnchor.constraint(equalToConstant: 32).isActive = true
        
        // Setup pathImageView
        self.pathImageView.centerXAnchor.constraint(equalTo: self.locationView.centerXAnchor, constant: 0).isActive = true
        self.pathImageView.centerYAnchor.constraint(equalTo: self.locationView.centerYAnchor, constant: 0).isActive = true
        
        // Setup ovalImageView
        self.ovalImageView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -43).isActive = true
        self.ovalImageView.topAnchor.constraint(equalTo: self.targetView.bottomAnchor, constant: 40).isActive = true
        
        // Setup rectangleThreeView
        self.rectangleThreeView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
        self.rectangleThreeView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
        self.rectangleThreeView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
        self.rectangleThreeView.heightAnchor.constraint(equalToConstant: 90).isActive = true
        
        // Setup group3View
        self.group3View.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 17).isActive = true
        self.group3View.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -18).isActive = true
        self.group3View.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 53).isActive = true
        self.group3View.heightAnchor.constraint(equalToConstant: 27).isActive = true
        
        // Setup menuLeftImageView
        self.menuLeftImageView.leadingAnchor.constraint(equalTo: self.group3View.leadingAnchor, constant: 0).isActive = true
        self.menuLeftImageView.centerYAnchor.constraint(equalTo: self.group3View.centerYAnchor, constant: 0).isActive = true
        
        // Setup groupImageView
        self.groupImageView.trailingAnchor.constraint(equalTo: self.group3View.trailingAnchor, constant: 0).isActive = true
        self.groupImageView.centerYAnchor.constraint(equalTo: self.group3View.centerYAnchor, constant: 0).isActive = true
        
    }


    // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
    // MARK: - Status Bar
    override public var prefersStatusBarHidden: Bool  {
        return true
    }

    override public var preferredStatusBarStyle: UIStatusBarStyle  {
        return .default
    }
}

这个文件包含 "版权 © 2018 Supernova"。这是一个有趣的细节,因为我在2020年3月做了这个导出。

该项目构建并成功运行。下面是它的外观。

image.png (iPhone11模拟器中的应用程序)

很明显,这些按钮不工作,因为我从未在Supernova Studio中添加逻辑。但这是有可能的。按钮没有绿色的背景。在我看来,这些问题并不重要,可以在导出后的代码中轻松修正。逻辑可以在Supernova Studio和IDE(Xcode或App Code)中添加。

外部库

有不同的布局框架。它们出现又消失,所以如果你选择这种方法,只要找到最近有更新(去年内)、支持Swift 5且有你需要的功能的框架即可。

让我们回顾一下最流行的一个--LinkedIn的LayoutKit。

let image = SizeLayout<UIImageView>(width: 50, height: 50, config: { imageView in
    imageView.image = UIImage(named: "earth.jpg")
})

let label = LabelLayout(text: "Hello World!", alignment: .center)

let stack = StackLayout(
    axis: .horizontal,
    spacing: 4,
    sublayouts: [image, label])

let insets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 8)
let helloWorld = InsetLayout(insets: insets, sublayout: stack)
helloWorld.arrangement().makeViews(in: rootView)

这个例子是从官方的LayoutKit网站借来的。

它创建标准的UIKit组件(像所有的框架一样)的方式和我们之前做的一样,但是它有两个额外的特点。

  • 代码更短,更清晰。
  • 它增加了自动调整组件大小的简便方法。

我想回顾的另一个框架是SnapKit。有时即使在基于Storyboard的项目中,你也需要从代码中创建组件。要在一个有约束的布局中添加一个组件,同时保持组件的尺寸不变,这是相当复杂的。

SnapKit可以帮助你用代码来添加约束。

import SnapKit

class MyViewController: UIViewController {

    lazy var box = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.addSubview(box)
        box.backgroundColor = .green
        box.snp.makeConstraints { (make) -> Void in
           make.width.height.equalTo(50)
           make.center.equalTo(self.view)
        }
    }

}

这个例子借用了官方的SnapKit网站

摘要

image.png (iOS的UI创建方法比较)

无论你选择什么,请不要忘记你的布局应该符合Apple Human Interface Guidelines

大多数iOS应用程序都有一个专门的文件夹,里面有资产。它通常是一个名为Assets.xcassets的文件夹。如果你创建一个新的Xcode项目,这个文件夹将被自动创建。

它包含不同类型的资产:图片、颜色、数据、AR资源、贴纸包和其他。最受欢迎的资产是图像集。图像集,除了图像,还有重要的元数据。例如,图像集可以为不同的屏幕尺寸、设备类型、分辨率提供不同的图像。集合中的图像可以被渲染成原始图像或模板。模板图片的颜色可以通过tint属性来改变。

资产文件夹有json文件来存储元数据。所有这些文件的名字都是一样的 - Contents.json。根部的Contents.json通常看起来像这样。

{
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}

我不认为有任何理由要改变这个文件。内部文件夹中的文件更有趣。这是一个AppIcon.appiconset'内Contents.json'文件的例子。

{
  "images" : [
    {
      "size" : "20x20",
      "idiom" : "iphone",
      "filename" : "Icon-App-20x20@2x.png",
      "scale" : "2x"
    },
    ...
    {
      "size" : "1024x1024",
      "idiom" : "ios-marketing",
      "filename" : "ItunesArtwork@2x.png",
      "scale" : "1x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}

"信息"部分也是如此。"images"包含不同设备和分辨率的图标图像数组。属性。

  • size - 以点为单位的图标大小
  • idiom - 设备类型
  • filename - 一个文件的名称。该文件应该在同一个文件夹中。
  • scale - 设备比例(2x或3x用于视网膜屏幕的设备,1x用于低分辨率的设备)。

图标集的Contents.json文件有一个类似的结构。例如。

{
  "images" : [
    {
      "idiom" : "universal",
      "filename" : "button_back.png",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "filename" : "button_back@2x.png",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "filename" : "button_back@3x.png",
      "scale" : "3x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}

我将把其他资产类型留在范围之外。它们更高级,并不是在所有项目中都使用。

资产文件夹可以在Xcode和AppCode中进行编辑。另外,如果你从一些网站下载图片集,例如material icons,你会得到imageset文件夹,里面有Contents.json。在所有其他情况下,你需要手动创建`json'文件。

假设你建立了一个应用程序,你得到了一个二进制文件(实际上,它是一个文件夹,但它里面有一个二进制文件)。你如何在iOS模拟器上运行它?

首先,你需要运行它。

AppCode可以为你运行它;在这方面它与Xcode非常相似。它支持代码调试,甚至在物理设备上运行。

如果你得到一个带有iOS应用程序的文件夹,你需要遵循三个步骤。

  1. 手动运行iOS模拟器。
  2. 将应用程序文件夹拖到运行中的iOS模拟器。
  3. 在虚拟屏幕上找到它并运行它。

要运行iOS模拟器,打开终端应用并运行此命令。

open -a Simulator.app

要选择设备型号和iOS版本,使用菜单文件打开设备硬件设备(在旧版本)。

调试

iOS模拟器有一个秘密。所有在其中运行的应用程序实际上是在你的Mac上运行的x86_64应用程序(在某种沙盒中)。这意味着你可以在本地调试iOS应用程序,与任何macOS应用程序的方式相同。

首先,运行lldb

lldb

其次,附加到一个进程。

(ldb) process attach --pid 12345

或者。

(lldb) process attach --name MyApp

你可以在Activity Monitor应用程序中找到你的应用程序,它安装在所有Mac上。在那里你可以找到你的应用程序的pid(进程ID)。

如果你不知道如何使用lldb,这里有文档

App签名

你完成了你的应用程序,在模拟器中进行了测试,并发现和修复了错误。现在是在真实设备上测试并上线的时候了。

首先,有几件事你需要知道。

  • iOS应用程序,与macOS应用程序一样是一个使用扩展名app的文件夹。
  • 要发布一个iOS应用,你需要创建一个名为ipa的档案。它是一个压缩包,里面有app文件夹和Payload文件夹。
  • 在制作`ipa'档案之前,你需要签署一个iOS应用程序。
  • 要签署你的iOS应用程序,你需要有一个苹果开发者账户。你可以在苹果开发者门户上创建一个。
  • 只有经过签名的应用程序才能在实体iOS设备上运行,即使是你自己的iPhone。
  • 你应该有一个证书和一个配置文件来签署一个应用程序。Xcode会自动创建它们,但如果你手动签署应用程序,你将不得不自己申请它们。
  • 在你的 "plist "文件中关于你的应用程序的信息(bundle id, entitlements)和你的provisioning profile应该相匹配。

这听起来很复杂,实际上也很复杂。让我们一步步回顾这个过程,因为如果没有应用签名,之前所有的努力都没有什么意义。

在这个例子中,我将使用前面 "用make从控制台构建iOS应用程序 "一节中构建的应用程序。它被称为MakeTest。

image.png (用make构建的iOS应用程序)

你的应用程序应该看起来像你在上图中看到的那样。白色标志意味着你不能在macOS中运行它。

第0步。编译一个应用程序

在我们开始之前,你应该有一个armv7arm64的应用程序。如果你从Xcode或其他平台构建它,只需选择一个合适的目标。如果你使用我们之前构建的例子,在Makefile中做一些修改。

  1. 替换掉:
SDKROOT:=$(shell xcrun --sdk iphonesimulator --show-sdk-path)

改为:

SDKROOT:=$(shell xcrun --sdk iphoneos --show-sdk-path)
  1. 改变构建命令,从:
clang -isysroot $(SDKROOT) -framework Foundation -framework UIKit -o MakeTest.app/$@ $^

改为:

clang -isysroot $(SDKROOT) -arch armv7 -arch arm64 -framework Foundation -framework UIKit -o MakeTest.app/$@ $^

这将产生一个所谓的 "胖 "二进制文件,有两种架构。这正是我们需要的。

如果你在这个阶段得到任何错误,你可能有Xcode的问题。安装它,如果你还没有这样做的话。运行它,每次更新后Xcode都会安装一个命令行工具。

步骤1. 创建一个证书

要创建一个证书,请打开这个链接。developer.apple.com/account/res… 。如果你在同一台Mac上用Xcode开发你的iOS应用程序,你可以跳过这一步。如果不是,请点击 "+"按钮并添加一个新的 "苹果分销 "证书。在这里我就不多说了。这个过程相当简单,你可以找到很多教程和手册。准备好后下载并安装。

在钥匙串访问应用程序中检查你的证书。你应该在下一步使用完整的证书名称。

image.png (带有苹果分销证书的钥匙串)

第2步。代签名

打开终端应用程序,并将当前目录改为你的工作文件夹(包含MakeTest.app)。

cd full_path

键入。

codesign -s "Apple Distribution: Your Account Name (TEAM_ID)" MakeTest.app

几秒钟后,你会看到MakeTest.app中的_CodeSignature文件夹。这是一个数字签名。而且它是不与你不信任的人分享你的证书的原因。这个签名证明了你开发了这个应用程序。如果有人窃取了你的证书,并发布了一个签名的应用程序,做了一些非法的事情,你的账户会被封锁。

第3步。创建一个配置文件

打开苹果供应门户。developer.apple.com/account/res…

点击 "+"按钮。你可以生成几种类型的配置文件。

  • iOS应用开发 - 可以只用于你自己的设备
  • AdHoc - 可以分发给有限的设备,包括在配置文件中
  • 应用商店 - 可以上传到应用商店

在这个例子中,让我们生成一个特设的配置文件,并为应用程序的安装创建一个链接。

在下一步,你需要选择你的应用程序ID。可能你的ID还不在列表中。

image.png (选择应用ID)

如果是的话,去标识符列表中添加一个新的App ID。developer.apple.com/account/res… 。请记住,你的应用程序ID(Bundle ID)应该与你的Info.plist中的ID相匹配。你可以选择你在应用程序中使用的功能。对于这个测试,你不需要选择任何东西。其中一些可以预选;不做修改。

回到配置文件生成,选择你的应用程序ID。在下一个屏幕上,你需要选择一个证书。它应该与你在上一步使用的证书完全相同。如果你有几个日期不同的证书,请检查你钥匙串中的证书。比较到期日。它应该是相同的,或者只有一天的差别。

在下一步,你需要选择设备。如果你以前使用过Xcode,你的设备可能已经注册了。如果没有,请在这里手动注册。

要注册一个新的设备,你需要输入它的UDID(唯一设备ID)。有两种方法可以得到它。

  • 你可以用电线把你的设备连接到你的PC/Mac,在iTunes中找到UDID。在macOS Catalina中,没有iTunes,但你可以在Finder中找到你的设备。在设备名称下,你会看到一些附加信息。点击它一次或几次,直到你看到UDID。请注意,序列号和UDID是不同的。
  • 你可以使用其中一个服务,如 get.udid.io 或类似服务。在你的iOS设备上打开它并按照指示操作。

当你的设备注册后,回到配置文件生成,选中一个或多个设备,并生成配置文件。在最后一步,你需要输入一个配置文件名称。通常情况下,我使用应用程序的名称加上 "AdHoc"。配置文件准备好后下载。

第4步。配置

要在你的应用程序中添加配置文件,只需将其复制到app文件夹中,并将其重命名为embedded.mobileprovision

然后再签署它。

codesign -f -s "Apple Distribution: Your Account Name (TEAM_ID)" MakeTest.app

如果你之前签署了它,添加-f标志(强制)。

第5步。权限

首先,让我们生成权利文件。

security cms -D -i Payload/MakeTest.app/embedded.mobileprovision

这将向控制台输出一个包括权利的大结构。将此结构保存在一个以.entitlements结尾的文件中。按照下面的结构。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>application-identifier</key>
    <string>TEAM_ID.com.test.make</string>
    <key>keychain-access-groups</key>
    <array>
        <string>TEAM_ID.*</string>
    </array>
    
    <key>get-task-allow</key>
    <false/>
    <key>com.apple.developer.team-identifier</key>
    <string>TEAM_ID</string>
</dict>
</plist>

你应该拥有与之前所有步骤相同的应用程序ID,并且TEAM_ID与你的苹果开发者账户相匹配。

你不应该在Payload中包含entitlements文件。相反,将其作为codesign命令的参数。

codesign -f -s "Apple Distribution: Your Account Name (TEAM_ID)" --entitlements 'MakeTest.entitlements' Payload/MakeTest.app

第6步。创建ipa

为了在物理设备上安装你的应用程序,你需要创建一个ipa档案。让我们看看如何做到这一点。

mkdir Payload
cp -r MakeTest.app Payload
zip -r MakeTest.ipa Payload

我们来了! 我们有一个存档 - MakeTest.ipa

第7步。分发

要分发你的应用程序,我推荐Diawi。Diawi(开发和内部应用无线安装)是一项服务,允许你上传你的ipa(或Android的apk),并获得一个链接和QR码。你把这个链接(和/或二维码)发送给设备所有者(iOS设备的UDID应该在你创建的配置文件中),他们只需轻点几下就可以安装。

问题是,在目前的配置下,你甚至不能上传,这就把我们带到了第8步。

第8步。更新Info.plist并排除故障

当你在Xcode中进行构建时,它会在签署前向Info.plist添加一些字段。

警告! 你应该在任何变化后更新你的应用程序签名,包括在 Info.plist 中的变化

这两个字段是上传发行版所必需的。

<key>CFBundleSupportedPlatforms</key>
<array>
  <string>iPhoneOS</string>
</array>
<key>MinimumOSVersion</key>
<string>10.0</string>

版本号可以是不同的,取决于你的目标iOS版本。

当你添加这些字段时,上传会成功,但你可能仍然无法安装该应用程序。生产版本的Info.plist应该包含兼容设备的信息。

我从Xcode生成的ipa中复制了这些值。其中一些是不必要的,但它们不会造成任何伤害。

<key>BuildMachineOSBuild</key>
<string>19D76</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>17B102</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>DTPlatformVersion</key>
<string>13.2</string>
<key>DTSDKBuild</key>
<string>17B102</string>
<key>DTSDKName</key>
<string>iphoneos13.2</string>
<key>DTXcode</key>
<string>1130</string>
<key>DTXcodeBuild</key>
<string>11C504</string>
<key>UIDeviceFamily</key>
<array>
    <integer>1</integer>
    <integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
<false/>

这是我的Info.plist文件的最终版本。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleDisplayName</key>
	<string>MakeTest</string>
	<key>CFBundleExecutable</key>
	<string>app</string>
	<key>CFBundleIdentifier</key>
	<string>com.test.make</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>MakeTest</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0.0</string>
	<key>CFBundleSignature</key>
	<string>MAKE</string>
	<key>CFBundleVersion</key>
	<string>1</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>

	<key>CFBundleSupportedPlatforms</key>
	<array>
		<string>iPhoneOS</string>
	</array>
	<key>MinimumOSVersion</key>
	<string>10.0</string>

	<key>BuildMachineOSBuild</key>
	<string>19D76</string>
	<key>DTCompiler</key>
	<string>com.apple.compilers.llvm.clang.1_0</string>
	<key>DTPlatformBuild</key>
	<string>17B102</string>
	<key>DTPlatformName</key>
	<string>iphoneos</string>
	<key>DTPlatformVersion</key>
	<string>13.2</string>
	<key>DTSDKBuild</key>
	<string>17B102</string>
	<key>DTSDKName</key>
	<string>iphoneos13.2</string>
	<key>DTXcode</key>
	<string>1130</string>
	<key>DTXcodeBuild</key>
	<string>11C504</string>
	<key>UIDeviceFamily</key>
	<array>
		<integer>1</integer>
		<integer>2</integer>
	</array>
	<key>UIRequiredDeviceCapabilities</key>
	<array>
		<string>arm64</string>
	</array>
	<key>UIRequiresFullScreen</key>
	<true/>
	<key>UIStatusBarHidden</key>
	<false/>
</dict>
</plist>

有可能你的应用程序还没有安装在你的设备上。很有可能,你不会看到任何错误。但如果你看到了,你如何解决它们的问题呢?

在你的Mac上打开Console应用程序。你的iOS设备应该被连接。

image.png (iOS设备控制台)

选择你的iOS设备,在过滤器中输入你的应用程序名称(右上角)。应用安装会产生大约50条信息,其中大部分没有太多的信息,所以可能需要几个小时才能找到并解决这个问题。

在上面的截图中,你可以看到一个权利文件的问题。如果你遵循了所有的步骤,就不应该有这个问题,但是如果你的情况比较复杂,或者你错过了某些步骤,你可以使用控制台应用程序来检查哪里出了问题。

完毕

如果你做的一切都对,你会看到你的应用程序安装在你的设备上。

如果你仍然有问题。

  1. 尝试添加应用程序的图标。你不需要资产目录。你可以只添加必要的png文件,并将它们添加到你的Info.plist中。
  2. 添加PkgInfo文件。老实说,我不明白它的用途,但所有Xcode生成的包都包括它。它只有8个字符。APPL????

不使用Xcode也完全可以创建iOS应用程序。如果你真的对Xcode有意见,你可能应该使用AppCode。你可以在那里写代码、构建和调试。它有许多插件,这将使这个过程更容易。

布局可以用许多不同的方式,使用代码或其他解决方案,如Supernova Studio或Sketch(有插件)。

仅仅使用终端和文本编辑器制作iOS项目是非常复杂的,但完全可以做到。这种方法只有在真的有必要的情况下才能使用;例如,用于自动构建。

下次见。编码愉快!


www.deepl.com 翻译