By Jiang Yi (Qinghan)
Apple launched Swift in 2014. After years of iteration, it finally achieved the Application Binary Interface (ABI) stability. This proves Swift to be a stable language suitable for apps in a production environment.
The Apple Worldwide Developers Conference (WWDC) 2019 released the SwiftUI framework, which is highly regarded by many Apple platform developers. According to unofficial sources, SwiftUI was conceptualized four years ago as the future of Apple's UI system. The dozens of core developers were not allowed to disclose any information about the project to other colleagues or the outside world. After the beta version was released this year, SwiftUI has proved, in my opinion, to be the most powerful mobile declarative programming framework currently available.
We have been continuously paying attention to the development of SwiftUI to study the possibility of applying SwiftUI to our business. Now, we can test this possibility with a stability monitoring dashboard.
Taobao provides an application used to monitor stability data. Generally, the data dashboard is displayed in PC browsers. We have used the data dashboard on PCs for many years. However, the Taobao app is an operation-oriented app, and some activities are often launched on holidays.
We need to develop an app that allows the personnel on duty or other related personnel to monitor stability data when they are not sitting in front of a PC.
It took 10 working days to develop the Swift on Taobao (SOT) app with the equivalent of 1.3 people working full time. It contains about 2,800 lines of Swift code.
The SOT app must be used within the internal network. Due to our lack of related experience, we spent a great deal of time on authentication for internal network access. We spent about five days on interface debugging, internal network authentication, and prototype design and spent another five days on developing SwiftUI. Our efficiency during this complete process was amazing.
Function design is at the core of app development. For the SOT app, stability monitoring involves monitoring the data dashboard, aggregating lists, analyzing the aggregation in detail, and analyzing crashes. The SwiftUI implementation plan was expected to take two weeks, so only the core and common functions of SOT were developed in Phase 1. Let's get to the design part.
For many programmers, user interface (UI) design is simple. The Mac platform provides design software such as Keynote and Sketch that are used in the same way as Storyboard. You need to learn how to use Storyboard if you only design a UI through code. We spent one day designing a simple UI.
The designed UI imitates the rounded corner and shadow design of the App Store. The reason for this imitation is that responsible design may make UI coding more challenging. It is much easier to use the system native style. However, this practice may result in many problems in an actual project.
In fact, a deep knowledge of SwiftUI largely depends on responsible UI design. We spent 6 hours developing the rounded corner design.
SwiftUI is a typical declarative UI programming framework for one-way data streams. In SwiftUI, a view is only a page description. It provides multiple data stream management objects. By modifying the values of the @State
, @Binding
, and @Obserabled data streams
, SwiftUI understands and rebuilds a view tree, which is similar to the virtual DOM, depending on the range of internal changes. Creating a view tree takes little time because each view is a structure. This provides robust performance assurance. Some bugs were found in actual projects, but they will be fixed in later versions of SwiftUI, given its fast iteration.
State is the most common proxy attribute in SwiftUI. After modifying the proxy attribute, SwiftUI automatically recalculates the body of the view internally to build a view tree. State is only modified in the body of the current view. Therefore, State is only applicable to operations that internally affect the current view. For example, in the case of downloading images from the Internet, the caller provides a URL and a placeholder image. Use State in SwiftUI as the downloaded images only affect the current view. For example, on the selection page of the SOT app, all images are from the Internet. Refer to the sample code below.
struct NetworkImage: SwiftUI.View {
var urlPath: String?
var placeHodlerImage: UIImage
init(url path: String?, placeHolder: String) {
self.urlPath = path
self.placeHodlerImage = UIImage(named: placeHolder)!.withRenderingMode(.alwaysOriginal)
}
@State var downLoadedImage: UIImage? = nil
var body: some SwiftUI.View {
Image(uiImage: downLoadedImage ?? placeHodlerImage)
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear(perform: download)
}
func download() {
if let _ = downLoadedImage {
return
}
_ = urlPath.flatMap(URL.init(string:)).map {
ImageDownloader.default.downloadImage(with: $0) { result in
switch result {
case .success(let value):
self.downLoadedImage = value.image.withRenderingMode(.alwaysOriginal)
case .failure(let error):
log.debug(error)
}
}
}
}
}
In conventional imperative programming, the most complex part of the GUI program is state management, and especially multi-data synchronization. A single data item exists in different components of a UI. Theoretically, each component synchronizes its changes to other components. More states and asynchronous operations may make the program less readable, resulting in bugs and increased difficulty during refactoring.
SwiftUI adheres to the Single Source of Truth (SSOT) concept, which refers to a single source of data. This concept was proposed long ago. However, it is not effectively implemented in many systems. For example, practitioners of functional reactive programming (FRP) may use Signals in RX or RAC for description purposes. FRP has a high application cost due to its complexity.
SwiftUI provides @Binding
as a solution. I once implemented binding in the same way as a simple closure, which is used to capture data from a single source. SwiftUI automatically refreshes the page to be synchronized. This simplifies data synchronization.
For example, the system provides a constructor for controls, which are a type of operable view. The constructor uses the @Binding
attribute to automatically synchronize the data source from the API caller.
For example, to implement the version and date selection function in the SOT project, we need to synchronize the control-selected values to the data source.
struct DateVersionPanel : View {
@Binding var version: String
@State var input = ""
@Binding var date: Date
var title: String
@State private var showVersionPicker = false
@State private var showDatePicker = false
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}
private func showDate() {
showDatePicker = true
}
var body: some View {
HStack(alignment: .center) {
Text(title)
.font(.system(size: 14))
HStack(alignment: .center) {
TextField(version.isEmpty ? "不区分版本" : version, text: $input, onEditingChanged: { (changed) in
log.debug("TextFieldonEditing: \(changed)")
}) {
log.debug("TextFielduserName: \(self.version)")
self.version = self.input
}
.font(.system(size: 9))
.padding(.leading, 20)
.frame(width: 100, height: 20)
NavigationLink(destination: VersionSelectView(version: $version)) {
Image("down_arrow")
.frame(width: 24, height: 14)
.aspectRatio(contentMode: .fill)
}
.offset(x: -20)
}
.frame(width: 100, height: 25)
.border(Color.grayText, width: 0.5)
.padding(.leading, 40)
NavigationLink(destination: CalendarView(date: self.$date)) {
HStack {
Text(dateFormatter.string(from: date) )
.font(.system(size: 9))
.padding(.leading, 10)
Image("down_arrow").padding(.trailing, 10)
}
.frame(width: 100, height: 25)
.border(Color.grayText, width: 0.5)
.padding(.leading, 40)
}
}
.padding(.bottom, 10)
}
}
ObservableObject was called ObjectBinding before Xcode11 Beta 4 was released. ObservableObject is a protocol that requires implementing a subject from the Combine framework. A subject is a special publisher that interacts with imperative programming. SwiftUI automatically subscribes to the subject and refreshes data when the subject sends changes.
ObservableObject might be used to synchronize data among multiple UI components and takes over the role of the controller in Model-View-Controller (MVC), a basic design pattern for Cocoa. ObservableObject is provisionally called ViewModel in the SOT project.
@Published
is a proxy attribute added after Xcode11 Beta 5. When @Published
is used in ObservableObject and sends a change, the subject of objectWillChanged in ObservableObject is automatically changed, and the page is automatically refreshed.
The Combine framework simplifies the multi-condition association. This provides many benefits for the SOT project. For example, the data dashboard has about a dozen data states. Data is refreshed when any data state is triggered.
class HomeViewModel: ObservableObject {
@Published var isCorrectionOn = true
@Published var isForce = false
@Published var crashType = CrashType.crash
@Published var pecision = Pecision.fifith
@Published var quota = Quota.count
@Published var currentDate = Date()
@Published var currentVersion = ""
@Published var comDate = Date().lastDay
@Published var comVersion = ""
@Published var refresh = true
@Published var metric: Metric? = nil
@Published var trends: [TrendItem] = []
@Published var summary: Summary? = nil
var api = SOTAPI()
// MARK: - Life Cycle
var cancels = [AnyCancellable]()
init() {
var cancel = $refresh.combineLatest($isForce, $isCorrectionOn)
.combineLatest($crashType, $pecision, $quota)
.combineLatest($currentDate, $currentVersion)
.combineLatest($comVersion, $comDate)
.debounce(for: 0.5, scheduler: RunLoop.main)
.sink {[weak self](_) in
self?.requestMetric()
self?.requestTrends()
}
cancels.append(cancel)
cancel = $refresh.sink{[weak self](_) in
self?.requestSummary()
}
cancels.append(cancel)
}
func requestMetric() {}
func requestTrends() {}
func requestSummary() {}
}
SwiftUI is a closed system with a limited number of controls. To meet development requirements, we need to program SwiftUI with the UIView class of UIKit. This helps to mitigate the migration burden and improve the capability of SwiftUI. In the SOT project, the date selection function depends on a third-party library and involves interaction with UIKit. SwiftUI provides a set of simple and clear standards that are implemented consistently across multiple platforms.
It's possible to repeatedly create the view container that conforms to the UIViewRepresentable protocol. If an internal data source requires a notification, create a coordinator to transfer the current container as a view, which is a structure. A copy operation creates the container. Therefore, just modify ObservableObject Binding in the coordinator.
struct CalendarView : UIViewRepresentable {
@Environment(\.presentationMode) var presentationMode
@Binding var date: Date
init(date: Binding<Date>) {
self._date = date
}
func makeUIView(context: UIViewRepresentableContext<CalendarView>) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
view.backgroundColor = .backgroundTheme
let height: CGFloat = 300.0
let width = view.frame.size.width
let frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
let calendar = FSCalendar(frame: frame)
calendar.locale = Locale.init(identifier: "ZH-CN")
calendar.delegate = context.coordinator
context.coordinator.fsCalendar = calendar
calendar.backgroundColor = UIColor.white
view.addSubview(calendar)
return view
}
func makeCoordinator() -> CalendarView.Coordinator {
Coordinator(self)
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<CalendarView>) {
log.debug("Date")
context.coordinator.fsCalendar?.select(date)
}
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
class Coordinator: NSObject, FSCalendarDelegate {
var control: CalendarView
var date: Date
var fsCalendar: FSCalendar?
init(_ control: CalendarView) {
self.control = control
self.date = control.date
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
self.control.date = date
}
}
}
The SOT project involved basic Combine operations. We did not design the architecture mode in detail because we focused on SwiftUI in Phase 1 of the project. Internal subscription exists in ViewModel. Imperative programming is applicable to a series of operations from sending network requests to synchronizing data. Such programming will be changed to reactive programming in Phase 2.
SwiftUI is a one-way data stream framework, following popular frameworks such as React, Flutter, and Reactive Native, which have been applied in large frontend projects. Redux is a popular architecture pattern for state management in one-way data stream frameworks and has been verified in many respects. Redux is applicable to SwiftUI.
Redux has the following core concepts:
1) The entire page or the app is a massive state machine with a state store, which is in a certain state at a certain time.
2) States are expressed in a tree structure on the page. The tree structure corresponds to the view tree in SwiftUI.
3) States cannot be directly modified by view operations. We can only indirectly modify the store by sending an action.
4) The reducer acquires newState by sending an action with oldState, in the form State = f(action+oldState)
.
The following figure is from the Redux Getting Started Tutorial by Ruan Yifeng.
This architecture pattern has been verified in large frontend projects and clearly presents user event interaction and state management. Currently, SwiftUI removes ViewCtonroller and adds ObserableObject and EmviromentObject, which are easy to use.
The preceding architecture pattern was not used in Phase 1 of the SOT project. We will explore a suitable architecture design pattern in Phase 2.
The SOT app was developed within 10 working days, indicating the extremely high development efficiency of SwiftUI. Though SwiftUI still has bugs, it will become the preferred solution to UI layout on the Apple platform. Going forward, we will continue to explore the implementation of SwiftUI in Taobao.
The SOT project is open source within Alibaba.
Are you eager to know the latest tech trends in Alibaba Cloud? Hear it from our top experts in our newly launched series, Tech Show!
An Alibaba Cloud Technical Expert's Insight Into Domain-driven Design: Domain Primitive
淘系技术 - May 11, 2020
Alibaba Clouder - April 1, 2020
Alibaba Clouder - April 17, 2020
Alibaba Clouder - July 19, 2019
Alipay Technology - November 26, 2019
Alibaba Clouder - November 2, 2020
Provides comprehensive quality assurance for the release of your apps.
Learn MoreWeb App Service allows you to deploy, scale, adjust, and monitor applications in an easy, efficient, secure, and flexible manner.
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreThis solution helps you improve and secure network and application access performance.
Learn MoreMore Posts by 淘系技术