Did you know you have the iOS IDE option?

The original address: betterprogramming. Pub/writing – ios…

Alex-nekrasov.medium.com/

Published: 13 April 2020 -22 minutes to read

IOS development without Xcode IDE (Image by Author)

Xcode is apple’s IDE (Integrated Development Environment), with a long history. It’s a native environment for iOS development, supports Objective-C and Swift, includes XIB and Storyboard editors, compilers, debuggers, and everything you need for development.

Why would anyone develop an iOS app without Xcode? There are several possible reasons.

  • Xcode consumes a lot of memory and runs slowly on many Macs.
  • Xcode has a lot of bugs. Apple fixes them, but adds new bugs with new features.
  • Xcode only works on macOS, which makes developers from other platforms uncomfortable.

Unfortunately, Apple does everything possible to prevent developers from using other platforms, the iOS emulator is part of the Xcode application package, there is no substitute for editing Storyboards, and doing a full signature build without Xcode is very complicated.

So, to be clear, some things are impossible, so don’t expect to overcome those limitations.

  • Native iOS apps can only be developed on the Mac. You can even write code in Windows or Linux, but you can’t build and sign there.
  • Non-native platforms, such as Flutter or React Native, cannot build iOS without a Mac.
  • Storyboards can only be edited in Xcode, so development without Xcode means development without Storyboards.
  • Every other IDE for iOS development requires Xcode. You don’t need to run it, but you should install it.
  • Signing and uploading apps to the App Store (or Test Flight) can be done from the command line (see below), but you need to have Xcode installed.

I’m not saying Xcode should never work. On the contrary, it’s the easiest way to make an iOS app. But if you’re facing one of the difficulties mentioned above, or just want to try something new, this story is for you.


Native or non-native?

IOS apps can be native or non-native. Well, it’s not black and white. There are a lot of choices in between.

Absolute native applications are written in Objective-C or Swift. Some parts can be written in C or C++. User interfaces are typically presented as storyboards or XIB files.

Absolute non-native apps are just websites wrapped in WKWebView (UIWebView). Apple now rejects such apps, but in the past they were fairly common. Obviously, such an app doesn’t need to interact with Xcode much, and creating a UIViewController with a WebView is hardly a problem, even if you rent a Mac online and have no experience with Xcode.

All other options are in the middle. They use native components, but the code is usually written in a non-native language. For example, Flutter uses Dart, React Native JavaScript, and Unity-C #. All of these frameworks use their own development environments, but they all export an Xcode project. You need to use…… Xcode to build its release. Usually, this is not a problem. The documentation includes step-by-step instructions for people who have never seen Xcode.

Writing a Flutter application in Android Studio is not the subject of this story. This is a default option, so we won’t waste time on it. We’ll discuss how to develop, test, and publish native iOS apps without Xcode.


What are our problems?

Let’s take a look at why it’s not so easy to write iOS apps without Xcode.

  • Create projects -Xcode saves your work in projects and workspaces. A workspace is simply a set of interrelated projects. They are folders with text files in them. The format of these files is proprietary, but they are large and have a lot of generated ids, so they should not be edited by humans.
  • User interface -Storyboard is another proprietary format. You can’t edit it outside of Xcode.
  • Build and test – There are command line compilers swiftc, GCC and LLVM, but how do you make a working iOS app?

The iOS project

Any application more complex than “Hello World “has more than one file. In the case of iOS, even “Hello World “has multiple files.

An iOS app that can run in the emulator has at least two components.

  • Executable binary file
  • The Info. The file

A full app that runs on a real iOS device has more components, but we’ll talk about that later.

To create a binary, you need at least two projects.

  • The source code
  • The build script

IOS apps can be built from the command line; For example, use make. We’ll see how to do that later.

A more comfortable way to build iOS apps is to use Xcode projects. I found only one application (besides Xcode) that could create such a project –AppCode. It is an app for JetBrain, similar to IDEA and Android Studio, but targeted at Apple specific developments, macOS and iOS. It runs only on macOS and is paid ($8.90 / month or $89 / year starting in April 2020).

Warning! AppCode cannot edit Storyboards. AppCode cannot edit Storyboards. When you try it, it opens Xcode. And it needs Xcode installed, otherwise, it won’t build the application.

Structure of Xcode projects

As I said earlier, Xcode projects should not be edited manually.

Xcodeproj is a folder that contains a file and several folders.

This file is project.pbxproj. It contains all the information about the project and is the most critical file.

Warning! If you are editing an existing project, make a backup.

Project. Bbxproj is a plist-like file. This format comes from the NeXTSTEP platform. Modern PList is XML, but project.bbxproj is more akin to JSON (although it is not).

Project. Bbxproj is primarily a set of objects. Each object has a unique identifier (a 96-digit number or a string of 24 hexadecimal characters). Objects can be source files, link frames, build phases, and so on. An object can be a group that contains other objects. Objects can have attributes and types. One of these objects is the root object of type PBXProject. Its type is PBXProject.

If, after all the warnings, you decide to edit the project.pbxproj file, you can use the Visual Studio Code Syntax Xcode Project Data extension. Here you can find detailed instructions for the file format.

In addition to project.bbxproj, the Xcode project contains several folders. They’re all options.

Project.xcworkspace is a workspace that contains only one project. When you open a project file in Xcode, it is automatically created and contains build patterns, breakpoint information, and other data that is not part of the project.

Xcuserdata is a folder that contains personal data for different users. If you’re the only developer, there’s only one folder. This folder is optional and can be excluded from Git and other repositories.

Xcshareddata is a folder containing shareddata between users; For example, schemes.

If you don’t use Xcode, you just need project.bbxproj.

Build iOS apps from the console with make

To be honest, I find it a pain to work on projects this way. It’s easier to resolve your differences with Xcode (which you need to install to sign applications anyway) than to do so manually. But the theoretical possibilities are interesting, so let’s take a closer look.

First, let’s get an SDK path.

xcrun --sdk iphonesimulator --show-sdk-path...
Copy the code

It’s going to look something like this.

/ Applications/Xcode. App/Contents/Developer/Platforms/iPhoneSimulator platform/Developer/SDKs/iPhoneSimulator13.4. The SDKCopy the code

In a Makefile, you can paste the output of xcRun, or use this command as part of a script.

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

So let’s make a target for our app.

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

Clang is a standard compiler in the Xcode package.

We added two frameworks.

  • Foundation
  • UIKit

The output file is maketest.app /app.

$@ is an automatic variable whose value is the name of a target. In our case, it’s app. $^ is another automatic variable. Its value is a complete list of dependencies — in this case main.m.

And the clean target:

.PHONY: clean
clean:
    rm MakeTest.app/app
Copy the code

Finally, let’s declare that app is the primary target.

default: app
Copy the code

This is a complete Makefile.

default: app

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

SDKROOT:=$(shell xcrun --sdk iphonesimulator --show-sdk-path)
app: main.m
	clang -isysroot $(SDKROOT) -framework Foundation -framework UIKit -o MakeTest.app/$@ $^
Copy the code

If you’ve never used make to build a project:

  • make appTo build aappThe target
  • make cleanClear (delete)appFile).

Since we declared a default target, we can build the project directly using the make command.

The next step is to create an APP folder. Yes, an iOS app is a folder.

mkdir MakeTest.app
Copy the code

There should be two files in maketest. app.

  • appIt’s a binary file that we usemakeCommand to build it.
  • Info.plist(capital I) is a list of attributes for a project. The iOS device or emulator needs to know which binary to run, which version it has, and other data.

This is our info.plist. If you run your own tests, you can change some fields.


      
<! DOCTYPEplist PUBLIC "- / / / / DTD PLIST Apple 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>
Copy the code

The last file is main.m. In a traditional iOS project, there are three different files:

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

This is just an organizational matter, not a hard and fast rule. Since all the components are very small, we put them together.

This is the main function of the App. Its sole purpose is to enter the main loop. We also passed the name of an application delegate, AppDelegate.

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

If you create a project with Xcode or AppCode, this function will be generated automatically.

Don’t forget to include UIKit in all objective-C files for your project.

#import <UIKit/UIKit.h>
Copy the code

If you’re from Swift, you probably don’t know that in Objective-C (and C++), every class must be declared and defined.

Class declaration.

@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong.nonatomic) UIWindow *window;
@end
Copy the code

The class definition.

@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
Copy the code

It creates a window, a view controller, and a label in its center. I’m not going to go into detail because it doesn’t concern coding outside of Xcode. This is just iOS programming.

Application delegates, view controllers, and other components can be placed in separate files. It’s even recommended. Technically, makefile-based projects can use the same structure as regular Xcode projects.

Here’s what it looks like.

An iOS project built without Xcode that runs in an iPhone 11 emulator.

Main.m complete source code.

#import <UIKit/UIKit.h>

@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

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

So, what’s next?

  • Permissions file – If you need to add any features to your iOS project, you need a permissions file. This is one with key-value pairsplistFile that describes which resources or services it uses on an iOS device or apple user account. You can be inApple documentationFor more details.

– As you can see, in our example, the application doesn’t take full screen. This can be addressed by adding Launch pictures or the appropriate size, or Launch screen storyboards.

  • Two goals: iOS devices and emulators. IOS builds require signatures, which we’ll get to later.

  • App ICONS can be added as part of the assets.xcassets folder or as a set of PNG files (referenced in info.plist). Assets.xcassets is a folder containing application Assets. We’ll come back to that later.

If you have any experience building commercial iOS apps this way, please leave a comment. I would be happy to add more information based on your experience.


Building the User Interface

I found four ways to build user interfaces without Xcode and Storyboards.

  • SwiftUI– Apple’s new UI builder can be edited in any text editor. For example, Visual Studio Code(VS Code) has a plug-in for writing Swift Code. And it’s free. The downside is that you need iOS 13 to run your app with SwiftUI. In a few years, it will no longer be a disadvantage, but it’s too early to abandon support for iOS 12 and earlier.

  • Create Components from Code — Any component can be created from Objective-C or Swift code without any UI designer. It’s long and uncomfortable, but very versatile. Code can be written anywhere, and your app can even run on the first iPhone (if you manage to build it for such an old device).

  • External tools – There are tools for converting designs (for example, done with Sketch) to native iOS code (in Objective-C or Swift). An example of such a tool is Supernova. It’s paid, just like any other tool with a similar function.

  • External libraries – These libraries allow you to write short code to build native UIs. They are free, but you need to learn to use them. It’s almost like learning a new programming language. Example: LinkedIn’s LayoutKit.

There is no perfect solution. Each of them has strengths and weaknesses, so it depends on what you use. I think in time SwiftUI will become the most popular UI building method for iOS. But if you need to get your app out faster, you’re better off using something else. Anyway, it’s your choice.

SwiftUI

SwiftUI is a new framework from Apple that is supposed to replace UIKit and Storyboards.

Advantage is

  • It’s native to iOS and Apple will support it, promote it, and incentivise developers to use it.
  • SwiftUI code can be written in any text editor.
  • SwiftUI can be mixed with UIKit in the same layout.

disadvantages

  • SwiftUI is only available for iOS 13 or later. It won’t run on iOS 12.
  • Layout preview and simulation only work in Xcode. There are no plug-ins for AppCode, VS Code, or any other Code editor.
  • Let’s see how SwiftUI works.
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(a)Text("California")
                        .font(.subheadline)
                }
            }
            .padding()

            Spacer()}}}struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()}}Copy the code

This example is borrowed from an official Apple tutorial.

Here you can find another detailed SwiftUI tutorial.

Create UIKit components from code

This is an absolutely universal way to create a layout. You can create components, you can add constraints, you can basically do anything. But each component needs to be fully initialized. And working with constraints is a nightmare when you can’t see what you’re doing. Previewing the layout can only be done on a Simulator or iOS device, and each revision requires a restart of the application.

That’s how it works.

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
}
Copy the code

This code should be in a UIViewController subclass.

Each UI component should be created this way. They can be nested, just like in storyboards. All the functionality of Storyboards and all the components of UIKit are available in code, including tables, collections, and so on.

External tools

Designers use Sketch, Adobe XD, Zeplin, or other tools for iOS layout. Wouldn’t it be great if you could export them directly into your iOS app? B: Sure. And it’s possible, but it’s not free.

I found several tools that allow this.

  • Supernova – from $20 / month
  • Code generation plugin for Sketch –47 euros/year; With Sketch, $99.
  • Zeplin has Swift and Objective-C extensions that export styles to iOS code. One program is free, then starts at $17 / month.

Please note that prices are not fixed and are subject to change.

The result of these tools/plug-ins is native iOS code, Swift or Objective-C.

Let’s say we have a design done with Sketch. In this example, I downloaded the waste Management App Sketch Resource file.

Sketch

The Sketch app opens with a warning (version mismatch), but this is not a problem. The next step — supernovae.

Supernova Studio

Supernova Studio has the ability to import Sketch or Adobe XD files.

Importing Sketch into Supernova Studio

The import was successful, but there were a few glitches. For example, the following arrow (see screenshot). Also, text overlaps with buttons during preview, but this should be fixed with a constraint or UIScrollView.

Supernova’s iOS screen design

The arrow problem can be solved by using raster images or manually. Let’s see how to export to iOS code. File → Export to iOS. When I try to export, a list of fonts I’m missing is displayed.

Missing fonts

Well, let’s leave it at that. The system font is fine.

I only selected the iPhone export, and only selected portrait so I wouldn’t delve into the details.

From supernova exit

The language can only be Swift. It makes sense to me to choose the latest one. Currently, it’s the Swift 5. UI must be Code. Other options are storyboards and XIBs, but they can only be opened by Xcode, so that’s not an option.

The export process took less than a minute. It generates an Xcode project with Podfile. The Podfile is almost empty; it has only a skeleton, but no dependencies.

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
Copy the code

Anyway, let’s install Pods to create a workspace.

Project generated by Supernova Studio

Each screen is a separate class, a subclass of UIViewController, in a different folder. Fonts, images, and other assets are also exported. And there are no Storyboards. That means we can open it with AppCode.

Here is an example of a Supernova Studio export, file 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(a)  {
        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(a)  {
        // Setup homeActiveNavView
        self.view.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1) /* #FFFFFF */
        self.view.translatesAutoresizingMaskIntoConstraints = true
        
        // Setup snazzyImage5ImageView
        self.snazzyImage5ImageView = UIImageView(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)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(a)  {
        self.extendedLayoutIncludesOpaqueBars = true
        
        self.navigationController?.setNavigationBarHidden(true, animated: true)}private func setupGestureRecognizers(a){}private func setupLocalization(a){}// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
    // MARK: - Layout
    override public func viewDidLayoutSubviews(a)  {
        super.viewDidLayoutSubviews()
        for layer in self.allGradientLayers {
            layer.frame = layer.superlayer?.frame ?? CGRect.zero
        }
    }

    private func setupLayout(a)  {
        // 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
    }
}
Copy the code

This file contains “Copyright © 2018 Supernova.” Which is an interesting detail because I made this export in March 2020.

The project is up and running successfully. Here’s what it looks like.

An iPhone 11 simulator app

Obviously, these buttons don’t work, because I’ve never added logic to Supernova Studio. But it is possible. Buttons don’t have a green background. In my opinion, these issues are trivial and can be easily fixed in the exported code. You can add logic to Supernova Studio and the IDE (Xcode or App Code).

External libraries

There are different layout frames. They come and go, so if you choose this approach, just find the most recently updated framework (within the last year) that supports Swift 5 and has the features you need.

Let’s review one of the most popular –LinkedIn’s 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)
Copy the code

This example is borrowed from LayoutKit’s official website.

It creates standard UIKit components (like all frameworks) in the same way we did before, but with two additional features.

  • The code is shorter and clearer.
  • It adds an easy way to automatically resize components.

The other framework I want to review is SnapKit. Sometimes, even in storyboard-based projects, you need to create components from code. Adding a constrained component to a layout while keeping its dimensions up to date can be quite complex.

SnapKit can help add constraints using code.

import SnapKit

class MyViewController: UIViewController {

    lazy var box = UIView(a)override func viewDidLoad(a) {
        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)
        }
    }

}
Copy the code

This example is based on SnapKit’s official website.

conclusion

Comparison of iOS UI creation methods

Whatever you choose, don’t forget that your layout should conform to Apple’s human interface guidelines.


assets

Most iOS apps have a special folder with assets in it. It is usually a folder called assets.xcassets. If you create a new Xcode project, this folder will be created automatically.

It contains different types of assets: images, colors, data, AR resources, sticker packs, and so on. The most popular asset is the image set. In addition to images, image sets also have important metadata. For example, image sets can provide different images for different screen sizes, device types, and resolutions. The images in the collection can be rendered as raw images or as templates. The color of the template image can be changed by using the hue property.

There are JSON files in the assets folder to store metadata. The name of these files is the same — contents.json. The root directory contents.json usually looks like this.

{
  "info" : {
    "version" : 1."author" : "xcode"}}Copy the code

I see no reason to change this file. Files in internal folders are more interesting. This is an example of a Contents. Json file within Appicon.Appiconset.

{
  "images": [{"size" : "20x20"."idiom" : "iphone"."filename" : "[email protected]"."scale" : "2x"},... {"size" : "1024x1024"."idiom" : "ios-marketing"."filename" : "[email protected]"."scale" : "1x"}]."info" : {
    "version" : 1."author" : "xcode"}}Copy the code

The same goes for the “information” section. Images “contains an array of icon images for different devices and resolutions. Properties.

  • Size – Icon size, in points
  • Idiom — Device type
  • Filename – Specifies the name of the file. The files should be in the same folder.
  • Scale – Device ratio (2x or 3x for retina screen devices and 1x for low resolution devices).

The Contents. Json file for the icon set has a similar structure. For example,

{
  "images": [{"idiom" : "universal"."filename" : "button_back.png"."scale" : "1x"
    },
    {
      "idiom" : "universal"."filename" : "[email protected]"."scale" : "2x"
    },
    {
      "idiom" : "universal"."filename" : "[email protected]"."scale" : "3x"}]."info" : {
    "version" : 1."author" : "xcode"}}Copy the code

I’m not going to say anything about other asset classes. They are more advanced and not used in every project.

The assets folder can be edited in Xcode and AppCode. In addition, if you download an image set from some site, such as the Footage icon, you’ll get a folder with contents.json in it. In all other cases, you need to create the JSON file manually.


Work with the iOS emulator

Let’s say you build an application and get a binary file (actually, it’s a folder, but it has a binary file in it). How do you run it on an iOS emulator?

First, you need to run it.

AppCode will run it for you, and it’s very similar to Xcode in that respect. It supports code debugging and can even run on physical devices.

If you get a folder with iOS apps, you need to follow three steps.

  1. Run the iOS emulator manually.
  2. Drag the apps folder to the running iOS emulator.
  3. Find it on the virtual screen and run it.

To run the iOS emulator, open the terminal application and run this command.

open -a Simulator.app
Copy the code

To select the device model and iOS version, use the menu “File “→” Open Device” or “Hardware “→” Devices” (in older versions).

debugging

The iOS emulator has a secret. All the applications running on it are actually x86_64 applications running on your Mac (in some kind of sandbox). This means you can debug iOS apps locally, just like any macOS app.

First, run the LLDB.

lldb
Copy the code

Second, attach to a process.

(lldb) process attach --pid 12345
Copy the code

Or.

(lldb) process attach --name MyApp
Copy the code

You can find your application in the Activity Monitor application, which is installed on all Macs. There you can find your application’s PID (process ID).

If you don’t know how to use LLDB, here’s the documentation.


Application signature

You finished your application, tested it in the simulator, and found and fixed bugs. Now it’s time to test and go live on real devices.

First, there are a few things you need to know.

  • IOS apps, like macOS apps, use extensionsapp“Folder.
  • To publish an iOS app, you need to create an app calledipaThe archive. This is a zip package,appThe folderPayloadInside the folder.
  • In the productionipaBefore archiving, you need to sign your iOS app.
  • To sign up for your iOS app, you need to have an Apple developer account. You can create one on the Apple Developer Portal.
  • Only signed apps will work on physical iOS devices, even your own iPhone.
  • You should have a certificate and a provisioning profile to sign an application. Xcode will create them automatically, but if you sign the app manually, you’ll have to apply for them yourself.
  • In yourplistThe information in the file about your application (Bundle ID, Entitlements) should match your Provisioning profile.

It sounds complicated, and it is. Let’s go through this process step by step, because without applying signatures, all of this effort doesn’t mean much.

In this example, I’ll use an application I built earlier in the section “Building iOS Apps from the console with Make.” Its name is MakeTest.

IOS app built with make

Your application should look like what you see in the image above. The white flag means you can’t run it in macOS.

Step 0. Compile an application

Before we begin, you should have an application for ARMV7 and ARM64. If you’re building from Xcode or another platform, just pick an appropriate target. If you use the example we built earlier, make some changes in the Makefile.

  1. Replacement:
SDKROOT:=$(shell xcrun --sdk iphonesimulator --show-sdk-path)
Copy the code

With.

SDKROOT:=$(shell xcrun --sdk iphoneos --show-sdk-path)
Copy the code
  1. Will build commands from.
clang -isysroot $(SDKROOT) -framework Foundation -framework UIKit -o MakeTest.app/$@ $^
Copy the code

To the.

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

This will result in a so-called “fat” binary with two architectures. This is exactly what we need.

If you get any errors at this stage, you may have problems with Xcode. Install it if you haven’t already done so. Run it, and Xcode installs a command-line tool after each update.

Step 1: Create a certificate Create a certificate

To create a certificate, please open this link: developer.apple.com/account/res… “+” button and then add a new “Apple Distribution “certificate. I won’t go into the details here. The process is fairly simple, and you can find many tutorials and manuals. Download and install when ready.

Check your certificate in Keychain Access. You should use the full certificate name in the next step.

Key chain with Apple distribution certificate

The second step. Code signing

Open the terminal application and change the current directory to your working folder (including Maketest.app).

cd full_path
Copy the code

Type.

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

After a few seconds, you’ll see the folder _CodeSignature inside makeTest. app. This is a digital signature. That’s why you should never share your credentials with anyone you don’t trust. This signature is proof of the application you developed. If someone steals your credentials, releases a signed app, or does something illegal, your account can be blocked.

The third step. Create a provisioning profile

Open the Apple Provisioning Portal:developer.apple.com/account/res…

Click the “+” button. You can generate several types of configuration files.

  • IOS app development — Only for your own device.
  • AdHoc – Can be assigned to a limited number of devices and included in a profile.
  • App Store – Upload to App Store.

In this example, let’s generate an AD hoc configuration file and create a link to install the application.

In the next step, you need to select your app ID. Maybe your ID isn’t in the list yet.

Select the application ID

If so, please list to identifier and add a new App ID:developer.apple.com/account/res… ID (Bundle ID) should match the ID in your info.plist. You can choose which features you use in your application. For this test, you don’t need to select anything. Some of these can be pre-selected and left unchanged.

Go back to configuration file generation and select your application ID. On the next screen, you need to select a certificate. It should be exactly the same as the certificate you used in the previous step. If you have several certificates with different dates, check the certificates in your Keychain. Compare due dates. It should be the same or one day different.

In the next step, you need to select the device. If you’ve used Xcode before, your device is probably registered. If not, register it manually here.

To register a new Device, you need to enter its UDID (Unique Device ID). There are two ways to get it.

You can wire your device to your PC/Mac and find the UDID in iTunes. In macOS Catalina, there’s no iTunes, but you can find your device in the Finder. Under the device name, you’ll see some additional information. Click it once or a few times until you see the UDID. Note that the serial number and UDID are different.

You can use a service such as get.udid. IO or something similar. Open it on your iOS device and follow the instructions. After your device is registered, go back to profile generation, check one or more devices, and generate the profile. In the last step, you need to enter a profile name. Usually, I use the name of the application with “AdHoc”. When ready, download the configuration file.

Step 4. configuration

To add the provisioning profile to your application, simply copy it to the app folder and rename it embedded. Mobileprovision.

Then sign it again.

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

If you have signed before, please add the -F flag (mandatory).

Step 5. The ownership

First, let’s generate the equity file.

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

This will output to the console a large structure including Entitlements. Save this structure in a file ending.Entitlements. Follow the following structure.


      
<! DOCTYPEplist PUBLIC "- / / / / DTD PLIST Apple 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>
Copy the code

You should have the same application ID as in all previous steps, and team_id matches your Apple developer account.

You should not include Entitlements files in Payload. Instead, make it an argument to the coDesign command.

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

Step 6: Create ipA

To install your application on a physical device, you need to create an IPA archive. Let’s see how we can do that.

mkdir Payload
cp -r MakeTest.app Payload
zip -r MakeTest.ipa Payload
Copy the code

Here we are! We have a file – maketest.ipa. We have a file – maketest.ipa.

Step 7. Distribute

To launch your app, I recommend Diawi. Diawi (Development with Internal App Wireless Installation) is a service that allows you to upload your IPA (or APK for Android) and get a link and QR code. You send the link (and/or QR code) to the device owner (the UDID for the iOS device should be in the Provisioning profile you create) and they can install it with a few clicks.

The problem is, under the current configuration, you can’t even upload it, which brings us to step 8.

Step 8: Update info.plist Update info.plist and troubleshoot

When you build in Xcode, it adds some fields to info.plist before signing it.

Warning! You should update your application signature after any changes, including those in info.plist.

These two fields are necessary for uploading the release.

<key>CFBundleSupportedPlatforms</key>
<array>
  <string>iPhoneOS</string>
</array>
<key>MinimumOSVersion</key>
<string>10.0</string>
Copy the code

The version number may vary, depending on your target iOS version.

When you add these fields, the upload will be successful, but you may still not be able to install the application. The production version of info.plist should contain information about compatible devices.

I copied these values from the IPA generated by Xcode. Some of it is unnecessary, but it doesn’t do any harm.

<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/>
Copy the code

This is the final version of my info.plist file.


      
<! DOCTYPEplist PUBLIC "- / / / / DTD PLIST Apple 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>
Copy the code

There’s a possibility that your app won’t be installed on your device yet. Chances are, you won’t see any mistakes. But if you do, how do you troubleshoot them?

Open the console application on your Mac. Your iOS device should already be connected.

Console for iOS Devices

Select your iOS device and enter your app name in the filter (top right). App installation produces 50 or so messages, most of which are small, so it can take hours to find and fix the problem.

In the screenshot above, you can see a problem with the rights file. This shouldn’t happen if you follow all the steps, but if your situation is more complicated or if you’ve missed some steps, you can use the Console application to check for problems.

complete

If you do everything correctly, you will see your application installed on your device.

If you still have questions.

Try adding an application icon. You don’t need an inventory. You can add the necessary PNG files directly and add them to your info.plist. Add the PkgInfo file. To be honest, I don’t understand what it does, but all xcode-generated packages include it. It’s only 8 characters long. APPL???? .


conclusion

You can create iOS apps without using Xcode. If you really have a problem with Xcode, you should probably use AppCode. You can code, build, and debug there. It has many plug-ins that will make the process much easier.

Layouts can be made in many different ways, using code or alternative solutions such as Supernova Studio or Sketch (with plug-ins).

Making an iOS project with just a terminal and a text editor is complicated, but possible. You should use this method only when you really need it; For example, for automated builds.

See you next time. Have fun coding!


Translation via www.DeepL.com/Translator (free version)