肇鑫的技术博客

业精于勤,荒于嬉

Speed up Xcode Building Time with CocoaPods for Beginners

At my first thought, this article had supposed to be named "The Modern Guide of Carthage with Xcode 12 for Beginners". However, while the process of learning Carthage and replace my own projects from using CocoaPods to Carthage. I found that using Carthage alone was not enough, or too complex for a one-week-learner like me.

The purpose of this article is to speed up your Xcode building and archiving time. In my own test, the speed could be over the original 10 times.

Since I am a beginner for Carthage, I may change this article a lot with updates.

Frameworks

A Framework is a prebuilt code package that you can use in your own code.

As programmers, we use frameworks a lot. For example, when you import UIKit, you are using UIKit framework provided by Apple.

UIKit is a build-in framework, so you just need to import it. There are also many other frameworks provided by third-parties. You can even create your own frameworks. For third-party frameworks, it is your responsibility to let Xcode know how to using them.

You can manually drag and drop those frameworks to Xcode. Or you can use framework tools like CocoaPods or Carthage.

CocoaPods VS Carthage

CocoaPods is more widely used. It has a long history ever since Objective-c. It uses source code so it has the most compatibility. Since you have to compile the framework from source code every time, it takes more time when build and archive.

Unless CocoaPods, Carthage builds the frameworks while you download them. Then you can drag and drop those frameworks to Xcode as if there are prebuilt. Those frameworks won't need to recompile again as they are already binaries. Your own project building time is saved.

We could use CocoaPods or Carthage alone, or mixed, or neither. The more you use CocoaPods, the complex the source codes are, the more build time of your own project lasts. So we intent to use Carthage alone. However, Carthage is not as compatible as CocoaPods with Xcode. Sometime you may find no solution to solve the problem of Carthage. Then you can try CocoaPods.

xcframework

Xcframework is the latest framework format that Xcode 12 supports and recommended.

Before xcframework, when we use a framework on macOS. The framework is easy to build. As for a Mac, only x86_64 architecture is used. If the framework is for iOS, things go complex. As if you could run an iOS app on both iOS simulators and iOS devices. For an iOS simulator running on a Mac, its architecture is x86_64, the same as the Mac. For an iOS device, the architecture is arm64. The framework you built should work in both x86_64 iOS simulators and arm64 iOS devices.

Framework tools use lipo to merge and split frameworks of different architectures.

$ man lipo

lipo - create or operate on universal files

A framework that contains more than one architectures is called a fat framework. Xcode could use the framework for both iOS simulators and iOS devices.

Apple Silicon Macs

Things go left when M1 Macs released. As they are arm based Macs instead of Intel based Macs. Everything on them is arm64 architecture.

For a framework runs on Mac, it now needs to be built into both x86_64 and arm64. lipo could do the trick.

For a framework runs on iOS, it now needs to be built into x86_64 and arm64 for iOS simulators and arm64 for iOS devices. You may think you can still use lipo. No, you can't. As you could see, there are arm64 in both iOS simulators and iOS devices. lipo could only specify a framework by its architecture, but it doesn't know if it is an iOS simulator or an iOS device. lipo cannot combine those frameworks together as the arm64 architectures are duplicated.


Xcframework comes. Xcframework is a bundle that contains an Info.plist with the information that the frameworks it contains. For each individual framework, it could be a fat framework or not.

Below is a picture of two built xcframeworks of Realm. The left is the Realm office prebuilt framework. The right is the framework built by Carthage by my Intel based iMac.

realm_official_VS_carthage

As we could see, for the left, there is an extra iOS maccatalyst framework. And for all simulators, there are extra arm64 architectures. I think it is because that xcframework was built on a M1 based Mac.

I didn't find the way to build arm64 architecture frameworks for simulators on an Intel based Mac. This leads to a bug of Xcode 12 on iOS app with watchOS app as companion.

Carthage by default can build xcframeworks with all architectures in all platforms, except Mac Catalyst.

Xcode 12 Requests arm64 Framework for watchOS Simulator

It is unnecessary to request arm64 framework for watchOS simulator on an Intel based Mac. It is a tradeoff. As if you are in a team that uses both Intel based and Mac based Mac, this feature is unnecessary. You can get rid of it manually.

Go to watchOS extension target -> Building Settings -> Excluded Architectures -> Debug, add "arm64".

exclude_ar

Carthage Working Steps

  1. Cartfile
  2. Resolve
  3. Checkout
  4. Build

Cartfile

Cartfile is the place where you put the framework's name and version. All frameworks that in Cartfile is called dependencies.

$ cat Cartfile
github "realm/realm-cocoa"

Resolve

When you call carthage update, carthage resolves the Cartfile as a Cartfile.resolved file. The resolved file may contain more frameworks than Cartfile as some frameworks request other frameworks.

$ cat Cartfile.resolved 
github "realm/realm-cocoa" "v10.7.2"

Checkout

Checkout downloads source codes of frameworks.

Build

The building process.

Install Frameworks to Xcode 12.4

To install frameworks to Xcode, basically we just need to drag and drop the frameworks to Xcode. However, depending the dropping locations, there are two methods of adding frameworks.

add_frameworks

Install

Cartfile

XCFrameworks

full platforms

how to installing frameworks

two ways:

  1. drag to frameworks
  2. drag to general
  3. differences

tips

remove unused framework
update arguments

known issues

  1. download binary instead of create xcframework
  2. download binary along with creation of xcframework

Xcode known issues

watchOS arm64 issue on simulator of Intel based Mac

Advances

build own framework with backups.

Strongly Recommend You to Add Your Own UINavigationControllers even in iOS 14 when Using UISplitViewController

In iOS 14, Apple adapts UISplitViewController from two columns to three columns. Especially, it says that

If you set a child view controller that’s not a navigation controller, the split view controller creates a navigation controller for it.

However, Apple didn't say that it will try to reuse the UINavigationController when you reset the columns.

That means, when you set .secondary view controller to another view controller, the split view controller will push the new view controller to the back stack, which is not what you want.

Issue Sample

UISplitViewController Issue

  1. run the sample
  2. you will see detail 01, press back
  3. you will see master 01, press back
  4. you will see primary, press Master 02
  5. you will see Master 02, press Master 02
  6. you will see Detail 02, press back

You expect to see Master 02.
You see Detail 01.

Work Around

Add a UINavigationViewController to each of detail view controllers.

UISplitViewController Issue work around

I have file this as a bug to Apple. The id is FB8640357.

Apple's Reply

Just got the reply from Apple. 8 days since I have filed this issue. Apple says:

After reviewing your feedback, we have some additional information for you:

This is expected behavior.

The secondary column gets this special treatment if the view controller gets set to a non-navigation controller when it already has a nav controller—i.e., it’s pushed onto the nav stack, as they discovered.

Expected use patterns would be:

• Change the contents of the detail view controller (not the vc itself) and send -showColumn:UISplitViewControllerColumnSecondary (if desired) to confirm that the secondary column is visible, or ...
• For the secondary column only, clear it first by setting its vc to nil, then setting new desired vc, or ...
• Manage the navigation controller in the secondary column directly with -setViewControllers: or push/pop. If you’re using an automatically created UINavigationController instead of one you provided, you can find it with -viewControllerForColumn: and then ask that vc for its navigation controller.

Apple gave 3 recommends. The first was easy to think of, but it needed the two view controllers to be very similar.

I like the second recommend most. func setViewController(_ vc: UIViewController?, for column: UISplitViewController.UISplitViewController.Column) does allow vc to be nil. And it is direct. There is some performance lose though. As it will create another UINavigationViewController each time.

The third recommend has the most performance. But it is indirect. And it smells bad. It should be seen as a workaround.

Final

I recommend to fix it in storyboard as I previously did. Or, using the second recommend by Apple.

Best Practice of URL Related Operations with Strings

What is a URL?

A Uniform Resource Locator (URL), colloquially termed a web address, is a reference to a web resource that specifies its location on a computer network and a mechanism for retrieving it.

https://en.wikipedia.org/wiki/URL

URL shown in address bar of a browser

In a browser like Safari or Chrome, a URL is shown in a human friendly way. For example, you may see a URL like this for https://zh.wikipedia.org/wiki/统一资源定位符, this is the Chinese version of URL page on Wikipedia.

https://zh.wikipedia.org/wiki/统一资源定位符

URL dealt by social media

However, the human friendly URLs are not standard and not allowed by social media platforms like Weibo and Twitter. Here are the results that sharing https://zh.wikipedia.org/wiki/统一资源定位符 on the above two platforms.

Weibo
url shared on weibo

Twitter
url shared on twitter

Both of them are not shown the URL correctly, as the Chinese characters are not allowed to be used in a URL directly.

URL with percentage encoding

In fact, those characters are converted by a method called percentage. Here is a percentage url:https://zh.wikipedia.org/wiki/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E5%AE%9A%E4%BD%8D%E7%AC%A6. The percentage URL can be used in social media platforms as well as in browsers.

Detecting URLs from Strings

We use NSDataDetector to get the URLs from a String.

let urlString = "This is a URL: https://zh.wikipedia.org/wiki/统一资源定位符"
let dataDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
dataDetector.matches(in: urlString, range: NSRange(location: 0, length: (urlString as NSString).length))
    .forEach {
        print($0.range) // {15, 37}
        print(urlString[Range($0.range, in: urlString)!]) // https://zh.wikipedia.org/wiki/统一资源定位符
        print($0.url!.absoluteString) // https://zh.wikipedia.org/wiki/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E5%AE%9A%E4%BD%8D%E7%AC%A6
}

The benefit of using NSDataDetector is that we could get the percentage URL automatically.

Bugs of NSDataDetector with link type

The above was enough for String and URLs. However, good things do not always happen. In practice, I found that there were some bugs in NSDataDetector which lead something fatal.

let s = """
// no issue
1. iOS版:https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8
2. iOS:https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8
3. iOS https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

// issue
4. iOS:https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8
5. iOS·https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8
6. iOSˆhttps://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8
7. iOSøhttps://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8
8. iOS_https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

// unknown scheme issue
9. iOShttps://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8
"""

let dataDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)

dataDetector.matches(in: s, range: NSRange(location: 0, length: (s as NSString).length))
     .enumerated().forEach { index, match in
        print(index + 1)
        print(match.url!.absoluteString)
        print(match.url!.scheme!)
        print(s[Range(match.range, in: s)!])
        print()
}

The result is

1
https://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
https
https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

2
https://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
https
https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

3
https://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
https
https://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

4
http://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
http
itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

5
http://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
http
itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

6
http://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
http
itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

7
http://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
http
itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

8
http://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
http
itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

9
iOShttps://itunes.apple.com/cn/app/%E5%92%95%E5%94%A72/id1366583897?mt=8
iOShttps
iOShttps://itunes.apple.com/cn/app/咕唧2/id1366583897?mt=8

From 4 to 8, the range of match dropped the https:// part from the original string. For the last, the scheme of the URL was not as expected.

Workaround

NSDataDetector is an API provided by Apple. We could file a bug and wait until Apple fixes it. Meanwhile, we should write a workaround and keep our apps working.

For issues in 4 to 8, we should double check if there were missing schemes ahead. If there was, we should recalculate the new NSRange and URL, as the old ones were not accuracy.

For the last issue, we thought it was a typing mistake and we would leave it alone.

import Foundation

extension NSTextCheckingResult {
    // FIXME: - Workaround for Apple API Issue
    public func extendedResultForHttp(of str:String) -> (NSRange, URL)? {
        guard resultType == .link else {
            return nil
        }
        
        guard url?.scheme?.lowercased().hasPrefix("http") ?? false else {
            return nil
        }
        
        // check bug with http:// or https://
        let httpScheme = "http://"
        let httpsScheme = "https://"
        let otherScheme = "x://"
        var location = self.range.location - (httpScheme as NSString).length
        
        if location >= 0 {
            let lowerBound = str.index(str.startIndex, offsetBy: location)
            let upperBound = str.index(lowerBound, offsetBy: httpScheme.count)
            
            if httpScheme == str[lowerBound..<upperBound] {
                let _nsRange = NSRange(location: location, length: (httpScheme as NSString).length + self.range.length)
                let url = URL(string: urlStringWithOriginalScheme(httpScheme)!)!
                
                return (_nsRange, url)
            }
        }
        
        location = self.range.location - (httpsScheme as NSString).length
        
        if location >= 0 {
            let lowerBound = str.index(str.startIndex, offsetBy: location)
            let upperBound = str.index(lowerBound, offsetBy: httpsScheme.count)
            
            if httpsScheme == str[lowerBound..<upperBound] {
                let _nsRange = NSRange(location: location, length: (httpsScheme as NSString).length + self.range.length)
                let url = URL(string: urlStringWithOriginalScheme(httpsScheme)!)!
                
                return (_nsRange, url)
            }
        }
        
        // check bug with other protocols
        location = self.range.location - (otherScheme as NSString).length
        
        if location >= 0 {
            let lowerBound = str.index(str.startIndex, offsetBy: location)
            let upperBound = str.index(lowerBound, offsetBy: httpsScheme.count)
            let schemeStr = String(str[lowerBound..<upperBound])
            let _nsRange = NSRange(location: 0, length: (schemeStr as NSString).length)
            let regularExpress = try! NSRegularExpression(pattern: "^[a-zA-Z]+://", options: .anchorsMatchLines)
            
            if regularExpress.firstMatch(in: schemeStr, range: _nsRange) != nil {
                return nil
            }
        }
        
        // good result
        return (self.range, self.url!)
    }
    
    private func urlStringWithOriginalScheme(_ originalScheme:String) -> String? {
        if let url = self.url, var scheme = url.scheme {
            scheme += "://"
            let str = url.absoluteString
            return originalScheme + String(str[str.index(str.startIndex, offsetBy: scheme.count)...])
        }
        
        return nil
    }
}

Others

Swift 5 String补遗