0 x00 | preface
It is assumed that you have a basic understanding of Swift syntax and have already experienced it. Although SwiftUI and Combine may not be immediately involved in our work, learning and using these two frameworks can provide us with an optimized idea from the side, which can change from the previous programming thinking of “process” and “command” to improve development efficiency.
The purpose of this sharing is to quickly gain a basic understanding of SwiftUI and Combine framework and verify the possibility of SwiftUI and Combine improving efficiency through a regular business demo. Share my problems and happy places in learning SwiftUI and Combine.
0x01 | SwiftUI
1. What is SwiftUI?
Instruction programmingResponsive programming.- Based on the
UIKit
,Core Graphics
,Core Text
The system framework encapsulates a complete and elegant DSL. - Combine responsive programming frameworks and functional programming ideas directly drive the flow of data in SwiftUI.
- It provides a set of universal syntax and basic data types to smooth out the differences of Apple’s own platforms and reduce the difficulty of cross-cutting with the same ecosystem.
- abandon
ViewController
Concept. - At the API level, there are echoes of RAC chain calls and a strongly dependent implementation of Combine.
2. What is Combine?
- SwiftUI data processing ontology, responsive framework.
- Provides bidirectional binding with data sources in SwiftUI.
- Streams handle “chain” calls. Unlike SwiftUI’s “chained” organizational UI, SwiftUI constructs an identified single object (synthetical sugar) through chain calls, but each chain call to Combine generates a new source of data.
0 x02 | implement a Context Menu
The container
Menu container
The “More Menu” is a component that almost all apps implement, and it takes on a secondary tool class business entry that is not the main business, but very important. If you do it in a normal UIKit way, the general implementation idea looks like this:
- To create a
UIWindow
或UIViewController
, as a container for the menu view; - through
UITableView
Or loop through components to create concrete menu views; - View relationship establishment and menu click event jump logic callback perfect.
If you just want to use SwiftUI to implement, in SwiftUI everything is View, there is no concept of ViewController, so the container here falls back to the View. Wrapping a view container might look like this: ·
struct MASSquareMenuView: View {
var body: some View {
GeometryReader { _ in
/ /...
}
.frame(minWidth: UIScreen.main.bounds.width,
minHeight: UIScreen.main.bounds.height)
}
}
Copy the code
MASSquareMenuView acts as the underlying ViewController. A View is actually a structure. If the body returns an indeterminate type, DSL parsing will fail, such as returning two views at the same time, using if-else judgment to return different views, which will be rejected. If we want to use an identifier to determine what view is being returned, we need to use a variable decorated with the @state keyword to do so.
Menu Cell container
struct MASSquareHostView: View {
var body: some View {
NavigationView {
// ...
ZStack {
MASSquareMenuView {
/ /...}}// ...}}}Copy the code
Chain calls
The “chain call process” is referred to as the View modifier in SwiftUI. After each modifier is called, it is returned to the next modifier in two cases: The first case is just for View (such as Text) font and other methods unrelated to the layout, return to the next modifier View of the same type; In the second case, the layout of the View is modified, such as the padding method is called, and the modifier returned to the next chain call is a brand new rewrapped View.
In fact, I think this is the same concept with the chain call library used before. Some chain method calls must depend on the first execution of some methods, for example, the size of the label Image must be customized, and the frame must be set resizeable before setting, otherwise it will fail.
The data source
SwiftUI’s API design philosophy forces me to think about the customization functions provided by the public components. I discussed with Mentor before whether such ContextMenu should be encapsulated as a UI component or a business component, and finally decided to make this menu component into a UI component.
The data source for “More Menus” was tweaked and we ended up with an API that basically conforms to the SwiftUI style, mostly due to the annoying addition of a Group. As mentioned earlier, SwiftUI does not accept multiple view returns. If you do want to return a “combined view” of multiple views, These views need to be returned manually wrapped as a View with a Group.
This raises a new problem, how to receive a set of views, and completely customize the contents of a menu component by passing a string of views to a component. With UIKit I might do this:
PJPickerView.showPickerView(viewModel: {
$0.titleString = "Relationship state"
$0.pickerType = .custom
$0.dataArray = [["Single"."On a date"."Married"]] {}) [weak self] finalString in
if let `self` = self {
self.loveTextField.text = finalString
}
}
Copy the code
In SwiftUI, however, the current version (Beta 7) is limited by the fact that it does not support returning uncertain content, so my design is:
MASSquareMenuView(isShowMenu: self.$showingMenuView) {
Group {
MASSquareMenuCell(itemName: "Notes",
itemImageName: "square.and.pencil") {
FirstView()}MASSquareMenuCell(itemName: "Square",
itemImageName: "burst") {
SecondView()}// ...}}Copy the code
ItemName and itemImageName can both be completed by ForEach. Currently, we have not found a good way to complete the dynamic jump.
dismantling
How to combine multiple sub-views in a relatively elegant way like the one above? My idea for this encapsulation approach comes from the way the List system components are used:
List {
// PJPostView(post: post)
ForEach(posts) { post in
PJPostView(post: post)
}
}
Copy the code
Let’s start with the definition of the system component List:
@available(iOS 13.0.OSX 10.15, tvOS 13.0, watchOS 6.0*),public struct List<SelectionValue.Content> : View where SelectionValue : Hashable.Content : View {
@available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue> >? The @ViewBuilder content: () -> Content)
@available(watchOS, unavailable)
public init(selection: Binding<SelectionValue? >? The @ViewBuilder content: () -> Content)
public var body: some View { get }
public typealias Body = some View
}
Copy the code
There is a brand new keyword @viewBuilder that requires the @ViewBuilder content closure to return a Content. Content is defined as follows:
@available(iOS 13.0.OSX 10.15, tvOS 13.0, watchOS 6.0*),public protocol ViewModifier {
associatedtype Body : View
func body(content: Self.Content) -> Self.Body
typealias Content
}
Copy the code
That is, any object in a content that can be “included” as long as it’s of type View is perfect, but what is @ViewBuilder? The definition in the documentation is:
@available(iOS 13.0.OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@_functionBuilder public struct ViewBuilder {
/// Builds an empty view from an block containing no statements, `{ }`.
public static func buildBlock(a) -> EmptyView
/// Passes a single view written as a child view (e.. g, `{ Text("Hello") }`) through
/// unmodified.
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
Copy the code
A View modified by @viewbubilder can receive multiple combined views. From the official documentation, we can see that a single component can hold up to 10 subcomponents at the same time. If there are more than 10 child components, the official recommendation is to abstract and encapsulate them into a new component.
The general menu Cell implementation details are as follows:
struct MASSquareMenuView<Content: View> :View {@Binding var isShowMenu: Bool
var content: () -> Content
var body: some View {
GeometryReader { _ in
VStack(alignment: .leading) {
self.content()
}
/ /...}}}Copy the code
When the MunuView is initialized, the init method is not supplemented with content, and since the last closure can be omitted in Swift 5.x, the previous API format is present.
0 x03 | Combine with CoreData
The significance of CoreData introduced here is only to provide a relatively stable data source, which has not been verified with network request for the time being.
The things this example wants to accomplish are:
- Enter the text content in the Pop-up box.
- Display all input content in the “home page”;
- Provide retrieval;
CloudKit
Backup.
To be honest, it took a while to work through this seamless logic. The main time is spent in understanding and adapting to the association between SwiftUI and Combine, and often thinking about how to organize various data sources reasonably and effectively to control the interaction of components. One of the must-haves is a “single data source,” limiting the source of a component’s behavior to the same data object itself.
The three most commonly used state modifiers are:
@State
;@Binding
;@ObservedObject
.
This is used in this example:
@State private var showingSheet = false
@Binding var text: String
@ObservedObject var aritcleManager = AritcleManager(a)Copy the code
Use @state to modify the showingSheet variable as an identifier that controls whether the “input box” pops up, use @binding to modify text to reference user input from the “popup box”, Decorate the aritcleManager object with @observedobject, which acts as the backbone connecting the data interaction on the home page.
As the data processing center of the homepage, AritcleManager undertakes two tasks of “input” and “search”. In order to guarantee the concept of single data source, @published is introduced to modify the real data source articles held inside the homepage. Whenever the articles change, Both issue notifications to external subscribers.
class AritcleManager: NSObject.ObservableObject {
/ / write 1
var objectWillChange: ObservableObjectPublisher = ObservableObjectPublisher(a)/ / write 2
@Published var articles: [Article] = []}Copy the code
With interactive USES NSFetchedResultsController to CoreData, this parts can be replaced with the network interactive methods:
// MARK: NSFetchedResultsControllerDelegate
extension AritcleManager: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
articles = controller.fetchedObjects as! [Article]
// Expression 2 can be omitted, and does not need to trigger publication actively
objectWillChange.send()
}
}
Copy the code
Initialization and interaction operations on the Home page are as follows:
struct MASSquareHostView: View {@ObservedObject var aritcleManager = AritcleManager(a)var body: some View {
NavigationView {
MASSquareListView(articles: self.$aritcleManager.articles,
showingSheet: self.$showingSheet) {
self.aritcleManager.articles[$0].delete()
}
}
}
}
Copy the code
From writing 1 found a strange place (writing 2 can be understood as a temporarily write 1 syntactic sugar), ObservableObjectPublisher how “automatic monitoring”? Take a look at the definition:
@available(iOS 13.0.OSX 10.15, tvOS 13.0, watchOS 6.0*),final public class ObservableObjectPublisher : Publisher {
public typealias Output = Void
public typealias Failure = Never
public init(a)final public func receive<S>(subscriber: S) where S : Subscriber.S.Failure= =ObservableObjectPublisher.Failure.S.Input= =ObservableObjectPublisher.Output
final public func send(a)
}
Copy the code
ObservableObjectPublisher is inherited from the Publisher, and the Publisher is one of three big pillars of Combine, concrete is defined as:
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0*),public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber.Self.Failure= =S.Failure.Self.Output= =S.Input
}
Copy the code
The three pillars in Combine
Publisher
, responsible for releasing events;Operator
Is responsible for converting events and data;Subscribe
Is responsible for subscribing to events.
All three are protocols and are concrete applications of @propertyWrapper.
Publisher
There are two main tasks for Publisher: publishing new events and their data, and preparing Subscriber subscriptions. Output and Failure define the types of values published by a Publisher and the types of errors that can occur.
Publisher can publish three kinds of events:
- A type of
Output
New value of: This represents a new value in the event flow; - A type of
Failure
Error: This represents a problem in the event stream and the event stream terminates. - Completion event: Indicates that all elements of the event flow have been published and the event flow terminates.
These three events of Publisher are not necessary, that is, Publisher may be issued only one or none at all, may be issued continuously and never stop, which is an infinite stream of events, and may indicate that no new events will be issued by issuing failure or FINISHED events. This is a finite flow of events.
Operator
Each Operator behaves in the same way: They use the data published by upstream publishers as input to generate new data, and then become new publishers themselves, and publish these new data as output to the downstream, which is equivalent to a responsive Publisher chain.
When the Publisher at the top of the chain publishes an event, each Operator in the chain processes the event and data. At the end of the chain we want to end up with events and data that directly drive the UI state. In this way, the consumer of the end can use the prepared data directly.
conclusion
Problem a: It is not suitable for direct use in the current “tree operation flow” project. Users’ operation of App is a “tree structure” in the current situation, but the strong dependence of SwiftUI and Combine leads to the development philosophy that a large number of compatible codes must be written to integrate Combine. However, Combine’s own “linear development model” is incompatible with the current model. So, the problem is not just a dependency on the system version.
SwiftUI currently does not have multi-line text components, so it can only use one layer of UITextView package. After the package is finished, it will get stuck when running on the simulator, so you can only use the real machine. In other words, trying to build a big thing from scratch based entirely on SwiftUI’s UI presentation layer is almost impossible and very, very painful.
, in my opinion, the two problems are solvable, especially the question 2, it is because it can perfect seamless compatible UIKit, on access costs are negligible, it is a problem of the impact of will be bigger, although Combine with Rx now wait a set has the same place, but the cost for the modification of the existing business, such as buried point, You may need to move from following the changes in the view to following the data flow.
SwiftUI is the same as SB and XIB. I think it is only a UI presentation layer and can be considered for top level operations such as layout. It should be used in the same way as SB and XIB.
Refer to the link
demo
Masq
Could you turn off the light
The related content
SwiftUI Tutorials
Some preliminary explorations of SwiftUI (1)
Some preliminary explorations of SwiftUI (2)
SwiftUI with Combine programming
It took five days to make an APP with SwiftUI. How did ali engineers do it?
How does SwiftUI implement a “more menu”?
How does SwiftUI integrate with Core Data?
Open source library
CombineX
MovieSwiftUI