Mixing UIKit and SwiftUI
Like most iOS / MacOS developers, I saw SwiftUI as one of the biggest announcements since Swift itself back in 2014. It allows a developer to move away from Storyboards / IB, as well as, write UI code that isn’t weighted with step-by-step directions on how the UI should display on the device.
I personally stopped using IB after 6 months of doing iOS development 10 years ago. I never liked, nor used Storyboards for any of my personal projects over the years as well. I wasn’t dismissive right away. I did try them out, they seemed very cumbersome and collaboration was always a PITA.
UIKit vs SwiftUI
During the week(s) after the announcement I read a lot of Tweets, comments and articles addressing: “What is better, UIKit or SwiftUI?” or “I’m a new iOS developer, should I bother learning UIKit or jump right into SwiftUI?”. You don’t have a binary choice. As a matter of fact it isn’t a true
or false
question, rather a multiple choice.
The answer is both.
UIKit has powered iOS since day 1. It is extremely powerful and what every iOS app has used for the past 10 years. That’s a lot of UIKit code Much like Swift itself, the SwiftUI adoption will come quickly. The ease of use and what you can accomplish with little code makes it a no brainer.
Even with the efficency and low barrier to entry to using SwiftUI there are limitations that only UIKit can overcome. If you are trying to build complex layouts or custom components then UIKit has you covered, but there is plenty of meat on the bone for what SwiftUI can do for you.
Beyond ContentView_Previews and Demos
“Damn, that’s really cool!” “You can do that with only how many lines?!”
These were literal reactions when I watched the sessions, demos and examples that Apple and other developers published, but I wondered how quickly could I use SwiftUI in my own apps.
Challenge Accepted
While working on Significant Other this summer I dropped iOS 12 support (because I can cool stuff like that). Without having to support a legacy OS I freed myself to see if (how) SwiftUI would play well with UIKit. I wasn’t going to replace all the UIKit code because a) no feasible b) this is the first release of SwiftUI which means changes are coming ( remember Swift 1–3?).
The settings screen within Significant Other is a static vertical scrolling CollectionView. The cells are pretty homogeneous. Bold title, short description and right justified button with a border and rounded corners.
The other two cell types are the avatar and the “standard” cell.
First Approach (spoiler, it didn’t work)
Nothing in this world is perfect and that includes SwiftUI. As I mentioned earlier it has its limitations. One of which is changing the color of the line separator in a list. Shocking I know. Since I couldn’t change the color of the separator I couldn’t use a List
. Naviely I thought I could just use a ScrollView
, a little creativity and be done. I was wrong. I was going to have to rethink my approach which could have included abandoning SwiftUI until I later date.
Second Apprach (spoiler, it worked)
Due to the lack of basic customization that I needed with a List
view my next approach was to use a ScrollView
and with a custom Rectangle
as the divider.
struct RowDivider: View { // MARK: Internal (properties)
var body: some View { Rectangle()
.fill(Color.white)
.frame(height: 1)
.padding(.leading, SettingsUIView.offset)
.padding(.trailing, SettingsUIView.offset)
}
}
Awesome…now I’m off to the races, EXCEPT I got a compile error. I checked, rechecked and checked again my code. Everything looked correct. A ScrollView
has a limit of 10 subviews (technically all containers do).
Group
to the rescue.
All containers in SwiftUI must return no more than ten children, which is usually fine for most purposes. However, if you need to have more than 10 views, if you need to return more than one view from your body property, or if you need to return several different kinds of view, you should use a group like this:
Source: https://www.hackingwithswift.com/quick-start/swiftui/swiftui-tips-and-tricks
Passing Data
Now that the UI is where I wanted it to be I needed to figure out how to pass data between the various SwiftUI views, as well as, the hosting view controller: SettingsViewController
, specifically:
- Handle
Button
actions for the different "cells" - Handle the avatar view being tapped
- Updating the setting’s avatar view
- Posting back to the current user’s record in CloudKit
- Updating the local cached instance of the image
Traditional patterns don’t really apply in these scenarios. Enter ObservableObject
and Published
from the [Combine](https://developer.apple.com/documentation/combine)
framework. While detail explainations for these are for another tutorial, they were the tools that provide the modern day solution to handling UI, state and data changes in your app.
In order to communicate back to the SettingsViewController
that a particular button in SettingsUIView
had been touched I use a PassthroughSubject
.
SettingsUIView
let subject = PassthroughSubject<SettingsDetailViewControllerType, Never>()
.
.
.
Group {
SettingsAvatarView(userManager: userManager, action: {
self.subject.send(.avatar)
})
}Group {
RowDivider()
SettingsInfoView(title: "Share",
subtitle: "Tell others about SO")
Spacer()
SettingsButtonStackView(title: "Share", action: {
self.subject.send(.share)
})
Spacer()
}
SettingsViewController
private let settingsDetailTypeSubscriber: SettingsDetailViewControllerTypeSubscriber = SettingsDetailViewControllerTypeSubscriber()
.
.
.
self.settingsUIView.subject.subscribe(self.settingsDetailTypeSubscriber)
.
.
.
settingsUIView.subject
.sink(
receiveCompletion: { completion in
print("Received completion (sink)", completion)
},
receiveValue: { value in
print("Received value (sink)", value)
switch value {
case .avatar:
self.presentionCameraViewController()
break
case .support:
self.sendEmail()
break
case .advanced:
self.pushTo(.advanced)
break
case .subscriptions:
self.pushTo(.subscriptions)
break
case .iap:
.
.
.
).store(in: &self.subscriptions)
That handles the “callbacks” when a user taps a button or the avatar image view, but how do I update the image for the avatar image view, as well as, any other view(s) that display my profile image?
Enter @Published
and @ObservedObject
.
In my UserManager
class I conform to ObservableObject
and mark the currentUserProfileImage
property as @Published
:
public final class SOUserManager: UserManager, ObservableObject{
@Published public private (set) var currentUserProfileImage: UIImage = SOUser.localProfileImage ?? UIImage(named: Constants.AppConstants.DefaultProfileImage)!
}
Whenever the the property is updated any other class / struct properties that are observing the UserManager
class will automagically get the updated property.
cloudkit.save(currentUser.record, callback: {saveResult inswitch saveResult {
case .success(_):
self.currentUserProfileImage = profileImage
SOUser.saveProfile(profileImage)
callback(true)
break
case .failure(.general(_)):
callback(false)
break
case .failure(.emptyresult(_)):
callback(false)
break
}
})
SettingsAvatarView
struct SettingsAvatarView: View {
// MARK: Internal (properties)
@ObservedObject var userManager: SOUserManager
var action: (() -> Void) = {}
var body: some View {
HStack {
Text("Upload Avatar").foregroundColor(.white).font(.custom("Avenir-Heavy", size: 15.0))
Spacer()
Image(uiImage: self.userManager.currentUserProfileImage)
.resizable().frame(width: 40.0, height: 40.0)
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.shadow(radius: 10)
.foregroundColor(.white)
}
.padding(SettingsUIView.offset)
.onTapGesture {
self.action()
}
}
}
Final Thoughts
SwiftUI has been amazing to work with despite its infanacy. It reminds me of the early days of Swift. Especially version 1–3. Lots of changes, headaches, but it also ushered in new areas of development. I feel the same way about SwiftUI. Digging in early to the API and developing best practices from day 1 will pay off huge in the next few years.
I’m taking the same approach I did with Swift. I would write some UI elements and utilities in Swift that I knew would work well with the other 99% of the Objective-C code. As the language matured and my confidence in using it increased I went Swift first. I have a feeling it will be “SwiftUI / Combine First” sooner rather than later.