Home Let's Build an Apple Watch App!
Post
Cancel

Let's Build an Apple Watch App!

Let’s Build an Apple Watch App! (Swift)

Step-by-step development of an Apple Watch App from scratch with watchOS 5

[Latest] Apple Watch Series 6 Unboxing & Two-Year Experience >>>Click Here

Introduction:

It’s been almost three months since my last Apple Watch Unboxing, and I finally found the opportunity to explore developing an Apple Watch App.

[Wedding App — The Largest Wedding Planning App](https://itunes.apple.com/tw/app/%E7%B5%90%E5%A9%9A%E5%90%A7-%E4%B8%8D%E6%89%BE%E6%9C%80%E8%B2%B4-%E5%8F%AA%E6%89%BE%E6%9C%80%E5%B0%8D/id1356057329#?platform=appleWatch){:target="_blank"}

Wedding App — The Largest Wedding Planning App

Here are my thoughts after using it for three months:

  1. e-sim (LTE) still hasn’t found a use case, so I haven’t applied for or used it yet.
  2. Frequently used features: unlocking Mac computers, checking notifications by raising the wrist, Apple Pay.
  3. Health reminders: After three months, I’ve started to slack off. I just glance at the notifications and don’t feel compelled to complete the rings.
  4. Third-party app support is still very poor.
  5. Watch faces can be changed according to mood, adding a sense of freshness.
  6. More detailed exercise records: For example, if I walk a bit further to buy dinner, the watch will automatically detect and ask if I want to record the exercise.

Overall, after three months of use, it still feels like a little life assistant, helping you with trivial matters, just as I wrote in the original unboxing article.

Third-party app support is still very poor

Before I actually developed an Apple Watch App, I was puzzled as to why the apps on Apple Watch were so basic, even just “usable,” including LINE (messages not synced and never updated), Messenger (just usable); until I actually developed an Apple Watch App and understood the developers’ difficulties…

First, understand the positioning of Apple Watch Apps, simplify complexity

The positioning of the Apple Watch is “not to replace the iPhone, but to assist”. This is the direction of official introductions, official apps, and watchOS APIs; hence, third-party apps feel basic and have limited functionality (sorry, I was too greedy Orz).

Take our app as an example, it has features like searching for vendors, viewing columns, discussion forums, online inquiries, etc.; online inquiries are valuable to bring to the Apple Watch because they require real-time and faster responses, which increases the chance of getting orders. Searching for vendors, viewing columns, and discussion forums are relatively complex features, and even if they can be done on the watch, it doesn’t make much sense (the screen can display too little information, and they don’t require real-time responses).

The core concept is still “assistive,” so not every feature needs to be brought to the Apple Watch; after all, users rarely have only the watch without the phone, and in such cases, the user’s needs are only for important features (like viewing column articles, which is not important enough to need to be viewed immediately on the watch).

Let’s get started!

This is also my first time developing an Apple Watch App, the content of the article may not be in-depth enough, please give me your advice!!

This article is only suitable for readers who have developed iOS Apps/UIKit basics

This article uses: iOS ≥ 9, watchOS ≥ 5

Create a new watchOS Target for the iOS project:

File -> New -> Target -> watchOS -> WatchKit App

File -> New -> Target -> watchOS -> WatchKit App

*Apple Watch Apps cannot be installed independently, they must be attached to an iOS App

After creating it, the directory will look like this:

You will find two Target items, both indispensable:

  1. WatchKit App: Responsible for storing resources and UI display /Interface.storyboard: Same as iOS, it contains the system default created view controller /Assets.xcassets: Same as iOS, stores the resources used /info.plist: Same as iOS, WatchKit App related settings
  2. WatchKit Extension: Responsible for program calls and logic processing (*.swift) /InterfaceController.swift: Default view controller program /ExtensionDelegate.swift: Similar to Swift’s AppDelegate, the entry point for Apple Watch App startup /NotificationController.swift: Used to handle push notifications on the Apple Watch App /Assets.xcassets: Not used here, I put everything in WatchKit App’s Assets.xcassets /info.plist: Same as iOS, WatchKit Extension related settings /PushNotificationPayload.apns: Push notification data, can be used to test push notification functionality on the simulator

Details will be introduced later, for now, just get a general understanding of the directory and file content functions.

View Controller:

In Apple Watch, the view controller is not called ViewController but InterfaceController. You can find the Interface Controller Scene in WatchKit App/Interface.storyboard, and the program that controls it is in WatchKit Extension/InterfaceController.swift (same concept as iOS)

The Scene is initially squeezed together with the Notification Controller Scene (I will pull it up a bit to separate them)

The Scene is initially squeezed together with the Notification Controller Scene (I will pull it up a bit to separate them)

You can set the title display text of the InterfaceController on the right.

The title color part is set by Interface Builder Document/Global hint, the style color of the entire App will be unified.

Component Library:

There are not many complex components, and the functions of the components are simple and clear

There are not many complex components, and the functions of the components are simple and clear.

UI Layout:

A tall building starts from the View. The layout part does not have Auto Layout, constraints, or layers like in UIKit (iOS). All layout settings are done using parameters, which is simpler and more powerful (the layout is somewhat like UIStackView in UIKit).

All layouts are composed of Groups, similar to UIStackView in UIKit but with more layout parameters

Group parameter settings

Group parameter settings:

  1. Layout: Set the layout method of the subviews contained within (horizontal, vertical, layered stacking)
  2. Insets: Set the margins of the Group (top, bottom, left, right)
  3. Spacing: Set the spacing between the subviews contained within
  4. Radius: Set the corner radius of the Group, that’s right! WatchKit comes with corner radius setting parameters
  5. Alignment/Horizontal: Set the horizontal alignment method (left, center, right) which will interact with the neighboring and outer wrapping views
  6. Alignment/Vertical: Set the vertical alignment method (top, center, bottom) which will interact with the neighboring and outer wrapping views
  7. Size/Width: Set the size of the Group, with three modes to choose from “Fixed: specify width”, “Size To Fit Content: determine width based on the size of the content subviews”, “Relative to Container: refer to the size of the outer wrapping view as the width (can set %/+- correction value)”
  8. Size/Height: Same as Size/Width, this item sets the height

Font/Font Size Settings:

You can directly apply the system’s Text Styles or use Custom (but I found that using Custom couldn’t set the font size); so I used System to customize the font size for each display Label.

Learning by Doing: Layout Example with Line

The layout is not as complicated as iOS, so I’ll demonstrate it directly through an example for you to get started quickly; using Line’s homepage layout as an example:

In WatchKit App/Interface.storyboard, find the Interface Controller Scene:

  1. The entire page is equivalent to UITableView used in iOS App development. In Apple Watch App, the operation is simplified, and the name is changed to “WKInterfaceTable”. First, drag a Table to the Interface Controller Scene.

Like UIKit UITableView, there is the Table itself and the Cell (called Row in Apple Watch); it is much simpler to use, you can directly design the layout of the Cell in this interface!

  1. Analyze the layout structure and design the Row display style:

To create a layout with a rounded full-width Image on the left and a stacked Label, and two evenly divided blocks on the right, with a Label on the top and another Label on the bottom.

2-1: Create the structure of the left and right blocks

Drag two Groups into the Group and set the Size parameters respectively:

Left green part:

Layout setting Overlap, the sub-View inside needs to stack the unread message Label

Layout setting Overlap, the sub-View inside needs to stack the unread message Label

Set a fixed square with a width and height of 40

Set a fixed square with a width and height of 40

Right red part:

Layout setting Vertical, the sub-View inside needs to display two items vertically

Layout setting Vertical, the sub-View inside needs to display two items vertically

Width setting refers to the outer layer, 100% ratio, minus the 40 of the left green part

Width setting refers to the outer layer, 100% ratio, minus the 40 of the left green part

Layout inside the left and right containers:

Left part: Drag in an Image, then drag in a Group containing a Label and align it to the bottom right (set the Group background color, spacing, and rounded corners)

Right part: Drag in two Labels, one aligned to the top left and the other aligned to the bottom left.

Naming the Row (same as setting the identifier for Cell in UIKit UITableView):

Select Row -> Identifier -> Enter custom name

Select Row -> Identifier -> Enter custom name

Are there multiple display styles for Rows?

Very simple, just drag another Row into the Table (which Row style to display is controlled by the program) and enter the Identifier name.

Here I drag another Row for displaying a no data prompt

Here I drag another Row for displaying a no data prompt.

WatchKit’s hidden does not occupy space, it can be used for interactive applications (display Table when logged in; display prompt Label when not logged in).

The layout is now complete, you can modify it according to your design; it’s easy to get started, practice a few more times, and play with the alignment parameters to get familiar!

Program Control Section:

Continuing with Row, we need to create a Class to reference the Row:

1
2
class ContactRow:NSObject {
}

1
2
3
4
5
6
7
8
class ContactRow:NSObject {
    var id:String?
    @IBOutlet var unReadGroup: WKInterfaceGroup!
    @IBOutlet var unReadLabel: WKInterfaceLabel!
    @IBOutlet weak var imageView: WKInterfaceImage!
    @IBOutlet weak var nameLabel: WKInterfaceLabel!
    @IBOutlet weak var timeLabel: WKInterfaceLabel!
}

Pull outlets, store variables

For the Table part, also pull the Outlet to the Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class InterfaceController: WKInterfaceController {

    @IBOutlet weak var Table: WKInterfaceTable!
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        
        // Configure interface objects here.
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
    
    struct ContactStruct {
        var name:String
        var image:String
        var time:String
    }
    
    func loadData() {
        //Get API Call Back...
        //postData {
        let data:[ContactStruct] = [] //api returned data...
        
        self.Table.setNumberOfRows(data.count, withRowType: "ContactRow")
        //If you have multiple ROWs to present, use:
            //self.Table.setRowTypes(["ContactRow","ContactRow2","ContactRow3"])
        //
        for item in data.enumerated() {
            if let row = self.Table.rowController(at: item.offset) as? ContactRow {
                row.nameLabel.setText(item.element.name)
                //assign value to label/image......
            }
        }
        
        //}
    }
    
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
        loadData()
    }
    
    //Handle Row selection:
    override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
        guard let row = table.rowController(at: rowIndex) as? ContactRow,let id = row.id else {
            return
        }
        self.pushController(withName: "showDetail", context: id)
    }
}

The operation of the Table is greatly simplified without delegate/datasource. To set the data, just call setNumberOfRows/setRowTypes to specify the number and type of rows, then use rowController(at:) to set the data content for each row!

The row selection event of the Table only requires overriding func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) to operate! (Table only has this event)

How to navigate between pages?

First, set the Identifier for the Interface Controller

First, set the Identifier for the Interface Controller

watchKit has two navigation modes:

  1. Similar to iOS UIKit push self.pushController(withName: Interface Controller Identifier, context: Any?)

Push method allows returning from the top left

Push method allows returning from the top left

Return to the previous page same as iOS UIKit: self.pop()

Return to the root page: self.popToRootController()

Open a new page: self.presentController()

  1. Tab display mode WKInterfaceController.reloadRootControllers(withNames: [Interface Controller Identifier], contexts: [Any?])

Or in the Storyboard, on the Interface Controller of the first page, Control+Click and drag to the second page and select “next page”

Tab display mode allows switching pages left and right

Tab display mode allows switching pages left and right

The two navigation methods cannot be mixed.

Passing parameters between pages?

Unlike iOS where you need to use custom delegates or segues to pass parameters, in watchKit, you can pass parameters by placing them in the contexts of the above methods.

Receive parameters in the InterfaceController’s awake(withContext context: Any?)

For example, if I want to navigate from page A to page B and pass an id: Int:

Page A:

1
self.pushController(withName: "showDetail", context: 100)

Page B:

1
2
3
4
5
6
7
8
9
override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        guard let id = context as? Int else {
           print("Parameter error!")
           self.popToRootController()
           return
        }
        // Configure interface objects here.
}

Programmatically controlling components

Compared to iOS UIKit, it is greatly simplified. Those who have developed for iOS should get the hang of it quickly! For example, label becomes setText() p.s. And surprisingly, there is no getText method, you can only use extension variables or store it in external variables

Synchronization/data transfer between iPhone and Apple Watch

If you have developed iOS-related Extensions, you might instinctively use App Groups to share UserDefaults. I was excited to do this initially, but I got stuck for a long time and found that the data never transferred. After checking online, I found that since watchOS 2, this method is no longer supported…

You need to use the new WatchConnectivity method to communicate between the phone and the watch (similar to the socket concept). Both the iOS phone and the watchOS watch need to implement it. We write it in a singleton pattern as follows:

Mobile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import WatchConnectivity

class WatchSessionManager: NSObject, WCSessionDelegate {
    @available(iOS 9.3, *)
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        // Mobile session activation completed
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        // Mobile received UserInfo from the watch
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        // Mobile received Message from the watch
    }
    
    // Additionally, didReceiveMessageData and didReceiveFile also handle data received from the watch
    // Decide which one to use based on your data transfer and reception needs
    
    func sendUserInfo() {
        guard let validSession = self.validSession, validSession.isReachable else {
            return
        }
        
        if userDefaultsTransfer?.isTransferring == true {
            userDefaultsTransfer?.cancel()
        }
        
        var list: [String: Any] = [:]
        // Add UserDefaults to the list...
        
        self.userDefaultsTransfer = validSession.transferUserInfo(list)
    }
    
    func sessionReachabilityDidChange(_ session: WCSession) {
        // Connection status with the watch app changes (when the watch app is opened/closed)
        sendUserInfo()
        // When the status changes, if the watch app is opened, sync UserDefaults once
    }
    
    func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?) {
        // Completed syncing UserDefaults (transferUserInfo)
    }
    
    func sessionDidBecomeInactive(_ session: WCSession) {
        
    }
    
    func sessionDidDeactivate(_ session: WCSession) {
        
    }
    
    static let sharedManager = WatchSessionManager()
    private override init() {
        super.init()
    }
    
    private let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil
    private var validSession: WCSession? {
        if let session = session, session.isPaired && session.isWatchAppInstalled {
            return session
        }
        // Return a valid and connected session with the watch app opened
        return nil
    }
    
    func startSession() {
        session?.delegate = self
        session?.activate()
    }
}

WatchConnectivity Code for iPhone

Add WatchSessionManager.sharedManager.startSession() in application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) of iOS/AppDelegate.swift to connect the session after launching the iPhone app.

For Watch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import WatchConnectivity

class WatchSessionManager: NSObject, WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
    }
    
    func sessionReachabilityDidChange(_ session: WCSession) {
        guard session.isReachable else {
            return
        }
        
    }
    
    func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?) {
        
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        DispatchQueue.main.async {
            //UserDefaults:
            //print(userInfo)
        }
    }
    
    static let sharedManager = WatchSessionManager()
    private override init() {
        super.init()
    }
    
    private let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil
    
    func startSession() {
        session?.delegate = self
        session?.activate()
    }
}

WatchConnectivity Code for Watch

Add WatchSessionManager.sharedManager.startSession() in applicationDidFinishLaunching() of WatchOS Extension/ExtensionDelegate.swift to connect the session after launching the Watch app.

WatchConnectivity Data Transfer Methods

To send data: sendMessage, sendMessageData, transferUserInfo, transferFile To receive data: didReceiveMessageData, didReceive, didReceiveMessage The methods for sending and receiving data are the same on both ends.

You can see that data transfer from the watch to the phone works, but data transfer from the phone to the watch is limited to when the watch app is open.

Handling Push Notifications in watchOS

The PushNotificationPayload.apns file in the project directory comes in handy for testing push notifications on the simulator. Deploy the Watch App target on the simulator, and after installation, launching the app will receive a push notification with the content of this file, making it easier for developers to test push notification functionality.

To modify/enable/disable PushNotificationPayload.apns, select the Target and then Edit Scheme

To modify/enable/disable PushNotificationPayload.apns, select the Target and then Edit Scheme.

watchOS Push Notification Handling:

Similar to iOS where we implement UNUserNotificationCenterDelegate, in watchOS we also implement the same methods in watchOS Extension/ExtensionDelegate.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import WatchKit
import UserNotifications
import WatchConnectivity

class ExtensionDelegate: NSObject, WKExtensionDelegate, UNUserNotificationCenterDelegate {

    func applicationDidFinishLaunching() {
        
        WatchSessionManager.sharedManager.startSession() // WatchConnectivity connection mentioned earlier
      
        UNUserNotificationCenter.current().delegate = self // Set UNUserNotificationCenter delegate
        // Perform any final initialization of your application.
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.sound, .alert])
        // Similar to iOS, this approach allows push notifications to be displayed even when the app is in the foreground
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        // When the push notification is clicked
        guard let info = response.notification.request.content.userInfo["aps"] as? NSDictionary, let alert = info["alert"] as? Dictionary<String, String>, let data = info["data"] as? Dictionary<String, String> else {
            completionHandler()
            return
        }
        
        // response.actionIdentifier can get the click event Identifier
        // Default click event: UNNotificationDefaultActionIdentifier
        
        if alert["type"] == "new_ask" {
            WKExtension.shared().rootInterfaceController?.pushController(withName: "showDetail", context: 100)
            // Get the current root interface controller and push
        } else {
           // Other handling...
           // WKExtension.shared().rootInterfaceController?.presentController(withName: "", context: nil)
            
        }
        
        completionHandler()
    }
}

ExtensionDelegate.swift

watchOS Push Notification Display, divided into three types:

  1. static: Default push notification display method

Along with the phone push notification, here the iOS side has implemented UNUserNotificationCenter.setNotificationCategories to add buttons below the notification; Apple Watch will also display them by default

Works with mobile push notifications, here the iOS side has implemented UNUserNotificationCenter.setNotificationCategories to add buttons below the notification; Apple Watch will also display them by default.

  1. dynamic: Dynamically handle push notification display styles (reorganize content, display images)
  2. interactive: Supported on watchOS ≥ 5, adds support buttons on top of dynamic

You can set the push notification handling method in the Static Notification Interface Controller Scene in Interface.storyboard

You can set the push notification handling method in the Static Notification Interface Controller Scene in Interface.storyboard

There’s not much to say about static, it just follows the default display method. Here we first introduce dynamic. After checking “Has Dynamic Interface,” a “Dynamic Interface” will appear where you can design your custom push notification presentation method (Buttons cannot be used):

My custom push notification presentation design

My custom push notification presentation design

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import WatchKit
import Foundation
import UserNotifications

class NotificationController: WKUserNotificationInterfaceController {

    @IBOutlet var imageView: WKInterfaceImage!
    @IBOutlet var titleLabel: WKInterfaceLabel!
    @IBOutlet var contentLabel: WKInterfaceLabel!
    
    override init() {
        // Initialize variables here.
        super.init()
        self.setTitle("結婚吧") // Set the title at the top right
        // Configure interface objects here.
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
    
    override func didReceive(_ notification: UNNotification) {
        
        if #available(watchOSApplicationExtension 5.0, *) {
            self.notificationActions = []
            // Clear the buttons added below the notification by iOS implementation of UNUserNotificationCenter.setNotificationCategories
        }
        
        guard let info = notification.request.content.userInfo["aps"] as? NSDictionary, let alert = info["alert"] as? Dictionary<String, String> else {
            return
        }
        // Push notification information
        
        self.titleLabel.setText(alert["title"])
        self.contentLabel.setText(alert["body"])
        
        if #available(watchOSApplicationExtension 5.0, *) {
            if alert["type"] == "new_msg" {
              // If it is a new message push notification, add a reply button below the notification
              self.notificationActions = [UNNotificationAction(identifier: "replyAction", title: "Reply", options: [.foreground])]
            } else {
              // Otherwise, add a view button
              self.notificationActions = [UNNotificationAction(identifier: "openAction", title: "View", options: [.foreground])]
            }
        }
        
        // This method is called when a notification needs to be presented.
        // Implement it if you use a dynamic notification interface.
        // Populate your dynamic notification interface as quickly as possible.
        
    }
}

The program part, similarly, drag the outlet to the controller and implement the functionality.

Next, let’s talk about interactive, which is the same as dynamic, but you can add more buttons and control the program with the same class as dynamic; I didn’t use interactive because I added my buttons using self.notificationActions, the difference is as follows:

Left uses interactive, right uses self.notificationActions

Left uses interactive, right uses self.notificationActions

Both methods require watchOS ≥ 5 support.

Using self.notificationActions to add buttons, the button events are handled by userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) in ExtensionDelegate, and actions are identified by identifier.

Drag Menu from the component library, then drag Menu Item, and then drag IBAction to the program control

Drag Menu from the component library, then drag Menu Item, and then drag IBAction to the program control

It will appear when you press hard on the page:

Content Input?

Use the built-in presentTextInputController method!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@IBAction func replyBtnClick() {
    guard let target = target else {
        return
    }
    
    self.presentTextInputController(withSuggestions: ["I'll reply later", "Thank you", "Feel free to contact me", "Okay", "OK!"], allowedInputMode: WKTextInputMode.plain) { (results) in
        
        guard let results = results else {
            return
        }
        // When there is input
        
        let txts = results.filter({ (txt) -> Bool in
            if let txt = txt as? String, txt != "" {
                return true
            } else {
                return false
            }
        }).map({ (txt) -> String in
            return txt as? String ?? ""
        })
        // Preprocess input
        
        txts.forEach({ (txt) in
            print(txt)
        })
    }
}

Summary

Thank you for reading this! You’ve worked hard!

This concludes the article. It briefly mentioned UI layout, programming, push notifications, and interface applications. For those who have developed iOS, getting started is really quick, almost the same, and many methods have been simplified to make it more concise, but the things you can do have indeed decreased (like currently not knowing how to load more for Table); currently, there are very few things you can do, and I hope the official will open more APIs for developers to use in the future ❤️❤️❤️

MurMur:

Deploying Apple Watch App Target to the watch is really slow — [Narcos](https://www.netflix.com/tw/title/80025172){:target="_blank"}

Deploying Apple Watch App Target to the watch is really slow — Narcos

If you have any questions or feedback, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here


This post is licensed under CC BY 4.0 by the author.

Apple Watch Series 4: Comprehensive Review from Unboxing to Mastery

iOS tintAdjustmentMode Property