In October, I participated in the Aurora Hackathon and spent one day writing a simple OCR application “ticket clip” for train tickets. Due to time and proficiency, I didn’t try the SwiftUI framework just launched by WWDC this year. Recently, I took time to rewrite it with SwiftUI + Combine and felt the charm of these two new frameworks. First of all, SwiftUI looks pretty good, but there are bugs and imperfections at present, so it is more suitable for Demo or personal functional projects that don’t care about design. Combine completion is ok, but Xcode’s automatic inference of complex closures often fails, which affects the coding experience.

SwiftUI is similar to the React framework in general, and has corresponding concepts. Generally, HStack and VStack are used as view hierarchs. Spacer is used to automatically fill the rest of the system, for example, in A horizontal HStack, a-spacer-b, where A is far left, B is far to the right.

In the process of using Swift UI, encountered some problems, share.

The default behavior of the view

  1. Common padding has a default value and is not zero.

  2. The List has a separator line by default. It is not possible to remove the separator line separately. The following code is used to remove the separator globally, and it is an unofficial method, after all, the List implementation may not be UITableView.

    List([]) {
    / /...
    }
    .onAppear {
       UITableView.appearance().separatorColor = .clear
    }
    Copy the code
  3. The order of attributes in a view affects presentation, as shown in the following two pieces of code

    / / 1
    HStack {
       Spacer()
    }
    .frame(height: 300)
    .background(Color.blue)
    .padding(30)
    Copy the code
    / / 2
    HStack {
       Spacer()
    }
    .frame(height: 300)
    .padding(30)
    .background(Color.blue)
    Copy the code

    To achieve the desired effect, use the first one, and the second one will be a blue rectangle with no margins, which I suspect is a Bug.

Data interaction

@State

This modifier is similar to React State, in that when State changes, it triggers a UI refresh where State is used. What works better than React is that you can use multiple modifiers to modify multiple variables separately, rather than putting them together, and then you don’t need to call setState to refresh, just a normal assignment to trigger the refresh.

@Binding

This modifier is used to solve the problem that the data is passed in from the upper layer, and when the upper layer data changes, it needs to notify the lower layer OF the UI refresh. In this case, the lower layer data should use the @binding modifier, so that the data like the @state modifier will not be copied in accordance with the value semantics, resulting in the data synchronization problem.

@EnvironmentObject

This modifier is used to solve the problem that when a lower level view wants to access data from a higher level, it can also be used within any nested level by declaring this modifier, in addition to passing the data at @binding.

@ObservedObject & ObservableObject

This modifier can be used when sharing a data model across multiple views, and can be used to consolidate existing data models into SwiftUI. Follow the Observable protocol and decorate the places that receive data changes with @observedobject so that all publishers in the Observable notify @Observedobject when they change. For properties that already exist, you can add the @Published modifier or send notifications using a custom Publisher.

/ / 1.
class GlobalModel: ObservableObject {@Published var name = "myName"
}
Copy the code
/ / 2.
class GlobalModel: ObservableObject {
    let didChange = PassthroughSubject<Void.Never> ()var name: = "myName" {
        didSet {
            didChange.send()
        }
    }
}
Copy the code

Implement modal display view

In App development, you need to Modal pop up UIViewController, in UIKit, you just need to say vc1. Present (vc2, Animated: True) this can be done in one line of code, but in SwiftUI it is a bit cumbersome.

struct ContentView: View {@State var isShowModal = false
    var body: some View {
        Button(action: {
            self.isShowModal = true{})Text("show")
        }
        .sheet(isPresented: $isShowModal) {
            ModalView(isShow: self.$isShowModal)
        }
    }
}

struct ModalView: View {@Binding var isShow:Bool
    
    var body: some View {
        Button(action: {
            self.isShow = false{})Text("dismiss")}}}Copy the code

As you can see, not only do you need to pass a flag bit to indicate whether to display, but you also need to change that state to tell the original view to disappear when it needs to be closed. This leads to unnecessary state passing and maintenance. I recommend defining closures for state transfer and facilitating data transfer between two views.

struct ContentView: View {@State var isShowModal = false
    var body: some View {
        Button(action: {
            self.isShowModal = true{})Text("show")
        }
        .sheet(isPresented: $isShowModal) {
            ModalView { intent in
                self.isShowModal = false
                / / intent}}}}struct ModalView: View {
    typealias Intent = String
    let onViewResult:((Intent?). - > ())var body: some View {
        Button(action: {
            self.onViewResult(nil) {})Text("dismiss")}}}Copy the code

Adaptation of UIKit

At this stage, even for new apps without any history, it is not practical to build all of them with SwiftUI, and it is normal to continue dealing with UIKit until some system views and third-party libraries are not adapted to SwiftUI.

SwiftUI UIView and UIViewController respectively provides UIViewRepresentable and UIViewControllerRepresentable protocol adapter. The requirements of the two protocols are almost the same, you just need to follow the protocol in one type and process the UIView or UIViewController that needs to be adapted in the required method, and this type can be used in the SwiftUI view.

class BView: UIView {}struct AView {}extension AView: UIViewRepresentable {
    func makeUIView(context: UIViewRepresentableContext<AView>) -> BView {
       	// Initialize UIView
        BView()}func updateUIView(_ uiView: BView, context: UIViewRepresentableContext<AView>){}}Copy the code

But a lot of times, UIKit views are not just UI presentations, they’re coupled with data changes, and there’s two streams of data: SwiftUI data to UIView, AND UIView data to SwiftUI (UIViewController is the same thing).

SwiftUI -> UIView

It’s pretty simple. The protocol provides the method.

class BView: UIView {
    var isDark:Bool = false {
        didSet {
            backgroundColor = isDark ? .black : .white
        }
    }
}

struct AView {@State var isDark = false
}

extension AView: UIViewRepresentable {
    func makeUIView(context: UIViewRepresentableContext<AView>) -> BView {
        BView()}func updateUIView(_ uiView: BView, context: UIViewRepresentableContext<AView>) {
      	/ / update the UIView
        uiView.isDark = isDark
    }
}
Copy the code

UIView -> SwiftUI

This case is slightly complicated. SwiftUI provides Coodinator to handle this case. In short, Coodinator is an intermediary that receives instances of UIView changes.

class BView: UIView {
    var isDark:Bool = false {
        didSet{ didChangeDark? (isDark) } }// UIKit commonly used data callback methods, closures, proxies, etc
    var didChangeDark:((Bool) - > ())? }struct AView {
  	// Attributes that need to receive changes
    @Binding var isDark: Bool
    
  	// Define Coordinator, which holds AView
    class Coordinator {
        let parent:AView
        
        init(_ view:AView) {
            parent = view
        }
    }
}

extension AView: UIViewRepresentable {
    // The implementation method
    func makeCoordinator(a) -> Coordinator {
        Coordinator(self)}func makeUIView(context: UIViewRepresentableContext<AView>) -> BView {
        let view = BView()
        view.didChangeDark = {
          	// Pass the change to the coordinator in the context
            context.coordinator.parent.isDark = $0
        }
        return view
    }
    
    func updateUIView(_ uiView: BView, context: UIViewRepresentableContext<AView>){}}Copy the code

Access to Combine

Combine Combine and SwiftUI directly is still a bit awkward, especially for common network requests, which are recommended via @obServedobject and ObservableObject. Here is an example of a direct combination of Combine and SwiftUI, which only provides the onReceive method for receiving.

struct ContentView: View {
  	// Request parameters
    @State var name = ""
    // Return the result
    @State var resultCode = 0
    
    // Request operations, such as network requests
    func fetch(_ name:String) -> AnyPublisher<Int.Error> {
        Just(name.isEmpty ? 0 : 1)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
    
    // Turn the request into an error of Never
    var nameRequest: AnyPublisher<Int.Never> {
        fetch(name)
        .catch { _ in
            Just(0)
                .setFailureType(to: Error.self)
        }
        .assertNoFailure()
        .eraseToAnyPublisher()
    }
    
    var body: some View {
        VStack {
            Button(action: {
                // Trigger the request
                self.name = "Request"{})Text("send request")}Text("code is \(resultCode)")}// Listen for requests. The error type must be Never
        .onReceive(nameRequest) { resultCode in
            self.resultCode = resultCode
        }
    }
}
Copy the code

The last

“Ticket clip” App can recognize train tickets in photos and automatically organize, display and summarize them. Written with SwiftUI + Combine, it basically does not use third-party libraries. It can be used as a practical example of SwiftUI.

Refer to the link

Interfacing with UIKit

SwiftUI data flow