The article links
While WWDC is over for the year, apple has delivered a number of surprises to iOS developers in the past month or so, with SwiftUI and Combine being the biggest new frameworks
Prior to that, UI development on iOS was not friendly to developers due to the lack of a declarative UI language at the system level. With iOS13, developers can finally move beyond the outdated layout system and embrace a new era of simpler and more efficient development. Combine, the responsive programming framework released with SwiftUI, offers a more elegant approach to development, meaning Swift has truly become a must learn language for iOS developers. This article is based on Swift5.1. How does SwiftUI Combine data binding
SwiftUI
Let’s start with an example. If we want to implement the login interface shown above, as we used to do with UIKit, then we need:
- To create a
UITextField
Is used to enter an account - To create a
UITextField
Is used to enter a password - To create a
UIButton
, set the click event to be the first twoUITextField
Text as data request
In the case of SwiftUI development, the code is as follows:
public struct LoginView : View {
@State var username: String = ""
@State var password: String = ""
public var body: some View {
VStack {
TextField($username, placeholder: Text("Enter username"))
.textFieldStyle(.roundedBorder)
.padding([.leading, .trailing], 25)
.padding([.bottom], 15)
SecureField($password, placeholder: Text("Enter password"))
.textFieldStyle(.roundedBorder)
.padding([.leading, .trailing], 25)
.padding([.bottom], 30)
Button(action: {}) {
Text("Sign In")
.foregroundColor(.white)
}.frame(width: 120, height: 40)
.background(Color.blue)
}
}
}
Copy the code
In SwiftUI, properties decorated with @state inform bound UI controls to force a rendering refresh when they change, thanks to the new PropertyWrapper mechanism. As you can see, SwiftUI controls are named almost the same as UIKit. The following table is the UI table of the two standard libraries:
SwiftUI | UIKit |
---|---|
Text | UILabel / NSAttributedString |
TextField | UITextField |
SecureField | UITextField with isSecureTextEntry |
Button | UIButton |
Image | UIImageView |
List | UITableView |
Alert | UIAlertView / UIAlertController |
ActionSheet | UIActionSheet / UIAlertController |
NavigationView | UINavigationController |
HStack | UIStackView with horizatonal |
VStack | UIStackView with vertical |
Toggle | UISwitch |
Slider | UISlider |
SegmentedControl | UISegmentedControl |
Stepper | UIStepper |
DatePicker | UIDatePicker |
View
Available (iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public protocol View: _View { /// The type of view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required `body` property. associatedtype Body : View /// Declares the content and behavior of this view. var body: Self.Body { get } }Copy the code
SwiftUI uses View to represent visual controls, but in reality it is quite different. View is a container protocol that does not present any content, but defines a set of interfaces for View interaction, layout, and so on. The UI control needs to implement the body in the protocol to return what needs to be displayed. View also extends the subscribe interface for Combine responsive programming:
Available (iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension View { /// Adds an action to perform when the given publisher emits an event. /// /// - Parameters: /// - publisher: The publisher to subscribe to. /// - action: The action to perform when an event is emitted by /// `publisher`. The event emitted by publisher is passed as a /// parameter to `action`. /// - Returns: A view that triggers `action` when `publisher` emits an /// event. public func onReceive<P>(_ publisher: P, perform action: @escaping (P.Output) -> Void) -> SubscriptionView<P, Self> where P : Publisher, P.Failure == Never /// Adds an action to perform when the given publisher emits an event. /// /// - Parameters: /// - publisher: The publisher to subscribe to. /// - action: The action to perform when an event is emitted by /// `publisher`. /// - Returns: A view that triggers `action` when `publisher` emits an /// event. public func onReceive<P>(_ publisher: P, perform action: @escaping () -> Void) -> SubscriptionView<P, Self> where P : Publisher, P.Failure == Never }Copy the code
@State
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyDelegate public struct State<Value> : DynamicViewProperty, BindingConvertible {
/// Initialize with the provided initial value.
public init(initialValue value: Value)
/// The current state value.
public var value: Value { get nonmutating set }
/// Returns a binding referencing the state value.
public var binding: Binding<Value> { get }
/// Produces the binding referencing this state value
public var delegateValue: Binding<Value> { get }
/// Produces the binding referencing this state value
/// TODO: old name for storageValue, to be removed
public var storageValue: Binding<Value> { get }
}
Copy the code
As one of the new features in Swift5.1, developers can wrap the IO implementation of variables into generic logic, modify the read and write logic with the keyword @propertyWrapper (updated to Beta4), and wrap variables in the form of @wrappername var variable. Use the example in WWDC Session 415 to encapsulate copy-on-write variables:
@propertyWrapper public struct DefensiveCopying<Value: NSCopying> { private var storage: Value public init(initialValue value: Value) { storage = value.copy() as! Value } public var wrappedValue: Value { get { storage } set { storage = newValue.copy() as! Value}} // update to beta4, <Value> /// beta3 uses wrapperValue named public var projectedValue: DefensiveCopying<Value> { get { self } } } public struct Person { @DefensiveCopying(initialValue: "") public var name: NSString }Copy the code
For variables wrapped in PropertyWrapper, a DefensiveCopying
variable named $name is generated by default. When updated, a Wrapper
parameter named _name is generated by default. Or the key variable declaration wrapperValue/projectedValue generated after access to the $name variable, the following two values access operations is the same:
extension Person {
func visitName() {
printf("name: \(name)")
printf("name: \($name.value)")
}
}
Copy the code
Combine
Customize handling of asynchronous events by combining event-processing operators.
As described in the official documentation, Combine is a standard library for handling asynchronous events by combining transform event operations. The relationship of event execution includes Observable and Observer, which correspond to Publisher and Subscriber in Combine
Asynchronous programming
Many developers think asynchronous programming will create threads execute tasks, most of the time the program really will create a thread when executed asynchronously, but this kind of understanding is not correct, the difference between synchronous and asynchronous programming programming only lies in whether the program will be blocked, waiting for the task has been completed below is for a period of no additional threads of asynchronous programming implementation code:
class TaskExecutor { static let instance = TaskExecutor() private var executing: Bool = false private var tasks: [() -> ()] = Array() private var queue: DispatchQueue = DispatchQueue.init(label: "SerialQueue") func pushTask(task: @escaping () -> ()) { tasks.append(task) if ! executing { execute() } } func execute() { executing = true let executedTasks = tasks tasks.removeAll() executedTasks.forEach { $0() } if tasks.count > 0 { execute() } else { executing = false } } } TaskExecutor.instance.execute() TaskExecutor.instance.pushTask { print("abc") TaskExecutor.instance.pushTask { print("def") } print("ghi") }Copy the code
One-way flow
If event A triggers event B, and vice versa, the two events can be thought of as one-way, like I’m hungry, so I eat, but it’s not like I eat, so I’m hungry. In programming, it would be much simpler if data flow could be one-way. As an example, here is a common UI code that does not flow in one direction:
func tapped(signIn: UIButton) {
LoginManager.manager.signIn(username, password: password) { (err) in
guard err == nil else {
ERR_LOG("sign in failed: \(err)")
return
}
UserManager.manager.switch(to: username)
MainPageViewController.enter()
}
}
Copy the code
In this code, the Action actually waits for State/Data to complete and then updates the View, which then accesses the Data to update its State. This logic allows Data to flow freely between different event modules, making both legibility and maintainability worse:
Once the flow between events is handled by the way of asynchronous programming, the person issuing the event does not care about the processing of waiting events, which undoubtedly makes the flow of data more single. Therefore, the significance of Combine lies in this. SwiftUI combines it to control the one-way flow of business data, reducing development complexity significantly:
Publisher
Available (OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public protocol Publisher { /// The kind of values published by this publisher. associatedtype Output /// The kind of errors this publisher might publish. /// /// Use `Never` if this `Publisher` does not publish errors. associatedtype Failure : Error /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)` /// /// - SeeAlso: `subscribe(_:)` /// - Parameters: /// - subscriber: The subscriber to attach to this `Publisher`. /// once attached it can begin to receive values. func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input }Copy the code
Publisher defines two information related to publishing: Output and Failure, which correspond to event Output values and Failure handling, and provides a Receive (Subscriber ๐ interface to register event subscribers. After iOS13, apple implemented many of Combine’s responsive interfaces based on the Foundation standard library, including:
-
The URLSessionTask can send a message when a request completes or when a request fails
-
NotificationCenter added a responsive programming interface
Use the official NotificationCenter extension as an example to create a Publisher for the login operation:
extension NotificationCenter {
struct Publisher: Combine.Publisher {
typealias Output = Notification
typealias Failure = Never
init(center: NotificationCenter, name: Notification.Name, Object: Any? = nil)
}
}
let signInNotification = Notification.Name.init("user_sign_in")
struct SignInInfo {
let username: String
let password: String
}
let signInPublisher = NotificationCenter.Publisher(center: .default, name: signInNotification, object: nil)
Copy the code
Also note: Self.Output == s. Output limits the data flow between Publisher and Subscriber must keep the same type, and it is difficult to maintain the consistency most of the time. So Publisher also provides higher-order map/compactMap functions to convert output values:
/ / / the Subscriber only receive user name information let signInPublisher = NotificationCenter. Publisher (center: the default, the name: signInNotification, object: nil) .map { return ($0 as? SignInfo)?.username ?? "unknown" }Copy the code
Subscriber
Available (OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public protocol Subscriber: CustomCombineIdentifierConvertible { /// The kind of values this subscriber receives. associatedtype Input /// The kind of errors this subscriber might receive. /// /// Use `Never` if this `Subscriber` cannot receive errors. associatedtype Failure : Error /// Tells the subscriber that it has successfully subscribed to the publisher and may request items. /// /// Use the received `Subscription` to request items from the publisher. /// - Parameter subscription: A subscription that represents the connection between publisher and subscriber. func receive(subscription: Subscription) /// Tells the subscriber that the publisher has produced an element. /// /// - Parameter input: The published element. /// - Returns: A `Demand` instance indicating how many more elements the subcriber expects to receive. func receive(_ input: Self.Input) -> Subscribers.Demand /// Tells the subscriber that the publisher has completed publishing, either normally or with an error. /// /// - Parameter completion: A `Completion` case indicating whether publishing completed normally or with an error. func receive(completion: Subscribers.Completion<Self.Failure>) }Copy the code
Subscriber defines a set of receive interfaces for receiving messages sent by Publisher. A complete subscription process is shown as follows:
After a successful subscription, receive(Subscription ๐ is called once, of the following type:
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public protocol Subscription: Cancellable, CustomCombineIdentifierConvertible { /// Tells a publisher that it may send more values to the subscriber. func request(_ demand: Subscribers.Demand) }Copy the code
Subscription is considered a single-subscription session that implements the Cancellable interface to allow Subscriber to unsubscribe halfway and release resources. Based on the NotificationCenter code above, complete the receiving part of Subscriber:
func registerSignInHandle() {
let signInSubscriber = Subscribers.Assign.init(object: self.userNameLabel, keyPath: \.text)
signInPublisher.subscribe(signInSubscriber)
}
func tapped(signIn: UIButton) {
LoginManager.manager.signIn(username, password: password) { (err) in
guard err == nil else {
ERR_LOG("sign in failed: \(err)")
return
}
let info = SignInfo(username: username, password: password)
NotificationCenter.default.post(name: signInNotification, object: info)
}
}
Copy the code
Combine with UIKit
Thanks to the new Swift5.1 feature, which is based on PropertyWrapper and Combine libraries, UIKit also has the ability to bind data flows. The default code is as follows:
class ViewController: UIViewController {
@Publishable(initialValue: "")
var text: String
let textLabel = UILabel.init(frame: CGRect.init(x: 100, y: 120, width: 120, height: 40))
override func viewDidLoad() {
super.viewDidLoad()
textLabel.bind(text: $text)
let button = UIButton.init(frame: CGRect.init(x: 100, y: 180, width: 120, height: 40))
button.addTarget(self, action: #selector(tapped(button:)), for: .touchUpInside)
button.setTitle("random text", for: .normal)
button.backgroundColor = .blue
view.addSubview(textLabel)
view.addSubview(button)
}
@objc func tapped(button: UIButton) {
text = String(arc4random() % 101)
}
}
Copy the code
A random number string is generated each time the button is clicked, and textLabel automatically updates the text
Publishable
When a string changes, the binding label needs to be updated. In this case, the PassthroughSubject class is used to impose a strong restriction on the output value type. The structure is as follows:
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final public class PassthroughSubject<Output, Failure> : Subject where Failure : Error {
public init()
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
/// Sends a value to the subscriber.
///
/// - Parameter value: The value to send.
final public func send(_ input: Output)
/// Sends a completion signal to the subscriber.
///
/// - Parameter completion: A `Completion` instance which indicates whether publishing has finished normally or failed with an error.
final public func send(completion: Subscribers.Completion<Failure>)
}
Copy the code
The Publishable implementation code looks like this (update 7.25) :
@propertyWrapper public struct Publishable<Value: Equatable> { private var storage: Value var publisher: PassthroughSubject<Value? , Never> public init(initialValue value: Value) { storage = value publisher = PassthroughSubject<Value? , Never>() Publishers.AllSatisfy } public var wrappedValue: Value { get { storage } set { if storage ! = newValue { storage = newValue publisher.send(storage) } } } public var projectedValue: Publishable<Value> { get { self } } }Copy the code
UI extensions
Extending controls with Extension supports property binding:
extension UILabel {
func bind(text: Publishable<String>) {
let subscriber = Subscribers.Assign.init(object: self, keyPath: \.text)
text.publisher.subscribe(subscriber)
self.text = text.value
}
}
Copy the code
Note that the Subscriber created will be held by libswiftCore of the system. If you fail to cancel all the Subscriber at the end of the controller life cycle, memory leakage will occur:
func freeBinding() { subscribers? .forEach { $0.cancel() } subscribers? .removeAll() }Copy the code
Finally put the running effect:
other
This year’s WWDC announcements are a good indication of apple’s ambitions, as Swift itself is a great language for writing DSLS, and the addition of two libraries to iOS13 makes development and maintenance costs much lower. Because of its high readability, developers can easily get used to the new library. At present, SwiftUI uses UIKit, CoreGraphics and other libraries in real time, which can be regarded as the abstract packaging layer based on these libraries. With the subsequent popularity of Swift, Apple’s bottom layer can replace UIKit and exist independently, and even realize the unification of large front end across platforms. Of course, the big front end on Apple is still a long way off, but it could be in the future
Refer to the reading
SwiftUI Session
Property Wrappers
Introduction to Combine
New Internet celebrity SwiftUI