SlideShare a Scribd company logo
Peter Friese, Developer Relations Engineer, Google
Building Reusable
SwiftUI Components
@peterfriese
SwiftConf 2023,Cologne
Peter Friese, Developer Relations Engineer, Google
Building Reusable
SwiftUI Components
@peterfriese
SwiftConf 2023,Cologne
Peter Friese
Developer Relations Engineer
Google
Peter Friese, Developer Relations Engineer, Google
Building Reusable
SwiftUI Components
SwiftConf 2023,Cologne
Why this talk?
@peterfriese
Help developers succeed
by making it easy to build
and grow apps
 +
Books?
Subscribe
for 20% off
Techniques for composing views
Building a simple component
Configuring views
Styling views
Distributing views
Building a simple component
Building a simple component
app
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
import SwiftUI
struct ContentView: View {
var body: some View {
List(0 !!" 5) { item in
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
}
import SwiftUI
struct ContentView: View {
var body: some View {
List(0 !!" 5) { item in
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
}
}
}
import SwiftUI
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(user.fullName)
}
}
}
}
import SwiftUI
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
HStack {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
Text(user.fullName)
}
}
}
}
import SwiftUI
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
HStack {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack {
Text(user.fullName)
Text(user.affiliation)
}
}
}
}
}
import SwiftUI
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
Text(user.affiliation)
}
}
}
}
}
import SwiftUI
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
}
}
}
Move to file
Extract Subview
Extract to local subview (property)
Extract to local subview (function)
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
}
}
}
♻ Extract Subview
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
ExtractedView()
}
}
}
struct ExtractedView: View {
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
ExtractedView()
}
}
}
struct ExtractedView: View {
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
Dear Apple…
Make Extract to Subview work all of the time
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
ExtractedView()
}
}
}
struct ExtractedView: View {
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
♻ Rename
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView()
}
}
}
struct AvatarView: View {
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
❌ Cannot find ‘user’ in scope
❌ Cannot find ‘user’ in scope
❌ Cannot find ‘user’ in scope
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView()
}
}
}
struct AvatarView: View {
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
❌ Cannot find ‘user’ in scope
❌ Cannot find ‘user’ in scope
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView()
}
}
}
struct AvatarView: View {
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
var user: User
❌ Cannot find ‘user’ in scope
❌ Cannot find ‘user’ in scope
Extract Subview
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(user: user)
}
}
}
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Dear Apple…
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Move to file
Extract Subview
Extract to local subview (property)
Extract to local subview (function)
Move to file
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(user: user)
}
}
}
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
♻ Move to file
Move to file New SwiftUI File
Move to file Make sure to use type name
Move to file
import SwiftUI
struct AvatarView: View {
var body: some View {
Text("Hello, World!")
}
}
#Preview {
AvatarView()
}
❌ Missing argument for parameter 'user' in call
Move to file
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
}
}
#Preview {
AvatarView()
Need to fix the preview
❌ Missing argument for parameter 'user' in call
Move to file
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
}
}
#Preview {
AvatarView(user: User.sample)
Dear Apple…
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Add Extract to File
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
}
}
#Preview {
AvatarView(user: User.sample)
}
What’s this?!
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
}
}
#Preview {
AvatarView(user: User.sample)
}
SwiftUI Layout Behaviour
Building layouts with stack views https://apple.co/448CaWL
Created by redemption_art
from the Noun Project
Created by redemption_art
from the Noun Project
Expanding Hugging
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
Created by redemption_art
from the Noun Project
Text is a content-hugging view
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
Created by redemption_art
from the Noun Project
VStack is a content-hugging view
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
Image is an expanding view
Created by redemption_art
from the Noun Project
… but we explicitly constrained it
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
HStack is a content-hugging view
Created by redemption_art
from the Noun Project
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
Spacer()
}
Spacer is an expanding view
Created by redemption_art
from the Noun Project
Move to file
Extract Subview
Extract to local subview (property)
Extract to local subview (function)
Extract to local subview (property)
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
Spacer()
}
}
}
♻ Extract to local subview
Extract to local subview (property)
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.affiliation)
.font(.subheadline)
}
var titleLabel: some View {
}
Text(user.fullName)
.font(.headline)
Extract to local subview (property)
struct AvatarView: View {
var user: User
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.affiliation)
.font(.subheadline)
}
var titleLabel: some View {
}
Text(user.fullName)
.font(.headline)
Extract to local subview (property)
struct AvatarView: View {
var user: User
var titleLabel: some View {
Text(user.fullName)
.font(.headline)
}
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.affiliation)
.font(.subheadline)
}
Spacer()
titleLabel
Dear Apple…
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Add Extract to File
Add Extract to local Subview
Move to file
Extract Subview
Extract to local subview (property)
Extract to local subview (function)
Extract to local subview (function)
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
titleLabel
Text("(Image(systemName: "graduationcap")) (user.jobtitle)")
.font(.subheadline)
Text("(Image(systemName: "building.2")) (user.affiliation)")
.font(.subheadline)
}
Spacer()
}
}
struct AvatarView: View {
var user: User
var titleLabel: some View {!!#}
Extract to local subview (function)
struct AvatarView: View {
var user: User
var titleLabel: some View {!!#}
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
titleLabel
Text("(Image(systemName: "graduationcap")) (user.jobtitle)")
.font(.subheadline)
Text("(Image(systemName: "building.2")) (user.affiliation)")
func detailsLabel(_ text: String, systemName: String) !$ some View {
}
Extract to local subview (function)
struct AvatarView: View {
var user: User
var titleLabel: some View {!!#}
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
titleLabel
Text("(Image(systemName: "building.2")) (user.affiliation)")
func detailsLabel(_ text: String, systemName: String) !$ some View {
}
Text("(Image(systemName: "graduationcap")) (user.jobtitle)")
.font(.subheadline)
Extract to local subview (function)
struct AvatarView: View {
var user: User
var titleLabel: some View {!!#}
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
titleLabel
Text("(Image(systemName: "building.2")) (user.affiliation)")
func detailsLabel(_ text: String, systemName: String) !$ some View {
}
Text("(Image(systemName: "graduationcap")) (user.jobtitle)")
.font(.subheadline)
Extract to local subview (function)
struct AvatarView: View {
var user: User
var titleLabel: some View {!!#}
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
titleLabel
Text("(Im
func detailsLabel(_ text: String, systemName: String) !$ some View {
}
Text("(Image(systemName:
.font(.subheadline)
"graduationcap")) (user.jobtitle)")
"(systemName)")) (text)")
Text("(Image(systemName: "building.2")) (user.affiliation)")
Extract to local subview (function)
struct AvatarView: View {
var user: User
var titleLabel: some View {!!#}
func detailsLabel(_ text: String, systemName: String) !$ some View {
Text("(Image(systemName: "(systemName)")) (text)")
.font(.subheadline)
}
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
titleLabel
Text("(Image(systemName: "building.2")) (user.affiliation)")
.font(.subheadline)
detailsLabel(user.jobtitle, systemName: "graduationcap")
detailsLabel(user.affiliation, systemName: "building.2")
Extract to local subview (function)
struct AvatarView: View {
var user: User
var titleLabel: some View {!!#}
func detailsLabel(_ text: String, systemName: String) !$ some View {
Text("(Image(systemName: "(systemName)")) (text)")
.font(.subheadline)
}
var body: some View {
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
titleLabel
detailsLabel(user.jobtitle, systemName: "graduationcap")
detailsLabel(user.affiliation, systemName: "building.2")
}
Techniques for composing views
Building a simple component
Configuring views
Styling views
Distributing views
Techniques for composing views
View body
import SwiftUI
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
HStack(alignment: .top) {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
Text(user.fullName)
.font(.headline)
Text(user.affiliation)
.font(.subheadline)
}
}
}
}
}
Local properties
View body
struct AvatarView: View {
var user: User
var titleLabel: some View {
Text(user.fullName)
.font(.headline)
}
var body: some View {
!!#
titleLabel
!!#
}
}
Local properties
View body
Local functions
struct AvatarView: View {
var user: User
func detailsLabel(_ text: String,
systemName: String) !$ some View {
Text("(Image(systemName: "(systemName)")) (text)")
.font(.subheadline)
}
var body: some View {
!!#
detailsLabel(user.jobtitle, systemName: "graduationcap")
detailsLabel(user.affiliation, systemName: “building.2")
!!#
}
}
Local properties
View body
Local functions
View Builders
View Builders
@ViewBuilder usage explained with code examples (SwiftLee) https://bit.ly/44bp37t
The @ViewBuilder attribute is one of the few result builders
available for you to use in SwiftUI.
You typically use it to create child views for a specific
SwiftUI view in a readable way without having to use any
return keywords.
“
View Builders
struct AvatarView: View {
var user: User
var titleLabel: some View {
Text(user.fullName)
.font(.headline)
}
func detailsLabel(_ text: String,
systemName: String) !$ some View {
Text("(Image(systemName: "(systemName)")) (text)")
.font(.subheadline)
}
var body: some View {
!!#
detailsLabel(user.jobtitle, systemName: "graduationcap")
detailsLabel(user.affiliation, systemName: “building.2")
!!#
}
Why no @ViewBuilder?
View Builders
struct AvatarView: View {
var user: User
var hero: some View {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
var body: some View {
HStack(alignment: .top) {
hero
VStack(alignment: .leading) {!!# }
}
}
}
View Builders
struct AvatarView: View {
var isRound = true
var user: User
var hero: some View {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
var body: some View {
HStack(alignment: .top) {
hero
VStack(alignment: .leading) {!!# }
}
}
}
❌ Branches have mismatching types 'some View'
View Builders
struct AvatarView: View {
var isRound = true
var user: User
var hero: some View {
if isRound {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
else {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
}
}
var body: some View {
Self.clipShape(_:style:)
Self.frame(width:height:alignment:)
View Builders
struct AvatarView: View {
var isRound = true
var user: User
var hero: some View {
if isRound {
AnyView {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
}
else {
AnyView {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
}
AnyView erases the
view’s type information
View Builders
struct AvatarView: View {
var isRound = true
var user: User
var hero: some View {
if isRound {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
else {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
}
}
@ViewBuilder @ViewBuilder keeps the
type information
View Builders
How to avoid using AnyView in SwiftUI (Tanaschita.com)
ViewBuilder vs. AnyView (Alexito's World)
https://bit.ly/45kVOQI
https://bit.ly/3OUbViE
Use @ViewBuilder if you want to return structurally
different views from a property / function.
“
Techniques for composing views
Building a simple component
Configuring views
Styling views
Distributing views
Configuring views
Configuring views
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(isRound: true, user: user)
}
}
}
Let’s turn this into a view modifier
: properties
Enum for the shape
enum AvatarImageShape {
case round
case rectangle
}
Environment Key
Enum for the shape struct AvatarImageShapeKey: EnvironmentKey {
static var defaultValue: AvatarImageShape = .round
}
enum AvatarImageShape {
case round
case rectangle
}
Environment Key
Enum for the shape
Extend Environment
struct AvatarImageShapeKey: EnvironmentKey {
static var defaultValue: AvatarImageShape = .round
}
enum AvatarImageShape {
case round
case rectangle
}
extension EnvironmentValues {
var avatarImageShape: AvatarImageShape {
get { self[AvatarImageShapeKey.self] }
set { self[AvatarImageShapeKey.self] = newValue }
}
}
Environment Key
Enum for the shape
Extend Environment
View modifier
struct AvatarImageShapeKey: EnvironmentKey {
static var defaultValue: AvatarImageShape = .round
}
enum AvatarImageShape {
case round
case rectangle
}
extension EnvironmentValues {
var avatarImageShape: AvatarImageShape {
get { self[AvatarImageShapeKey.self] }
set { self[AvatarImageShapeKey.self] = newValue }
}
}
extension View {
func avatarImageShape(_ imageShape:
AvatarImageShape) !$ some View {
environment(.avatarImageShape, imageShape)
}
}
Environment Key
Enum for the shape
Extend Environment
View modifier
Update view code
struct AvatarView: View {
var isRound = true
var user: User
@ViewBuilder
var hero: some View {
if isRound {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
else {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
}
}
!!#
}
var isRound = true
Environment Key
Enum for the shape
Extend Environment
View modifier
Update view code
struct AvatarView: View {
var user: User
@ViewBuilder
var hero: some View {
if isRound {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
else {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
}
}
!!#
}
var isRound = true
@Environment(.avatarImageShape) var imageShape
Environment Key
Enum for the shape
Extend Environment
View modifier
Update view code
struct AvatarView: View {
var user: User
@ViewBuilder
var hero: some View {
if {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
.clipShape(Circle(), style: FillStyle())
}
else {
Image(user.profileImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 75)
}
}
!!#
}
@Environment(.avatarImageShape) var imageShape
imageShape !% .round
Environment Key
Enum for the shape
Extend Environment
View modifier
Update view code
Usage at the call site
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(user: user)
.avatarImageShape(.rectangle)
}
}
}
Environment Key
Enum for the shape
Extend Environment
View modifier
Update view code
Usage at the call site
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(user: user)
.avatarImageShape(.rectangle)
}
}
}
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(user: user)
}
.avatarImageShape(.rectangle)
}
}
Environment Key
Enum for the shape
Extend Environment
View modifier
Update view code
Usage at the call site
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(user: user)
}
.avatarImageShape(.rectangle)
}
}
struct ContentView: View {
@State var users = User.samples
var body: some View {
List(users) { user in
AvatarView(user: user)
.avatarImageShape(
user.isTalking
? .round
: .rectangle)
}
.avatarImageShape(.rectangle)
}
}
Configuring views
struct ContentView: View {
var body: some View {
AvatarView(user: User.sample)
.onEditProfile {
print("onEditProfile triggered")
}
}
}
Let’s register an action handler
: action handlers
Environment Key
struct AvatarEditProfileHandler: EnvironmentKey {
static var defaultValue: (() !$ Void)?
}
Extend Environment
Environment Key
struct AvatarEditProfileHandler: EnvironmentKey {
static var defaultValue: (() !$ Void)?
}
extension EnvironmentValues {
var editProfileHandler: (() !$ Void)? {
get { self[AvatarEditProfileHandler.self] }
set {
self[AvatarEditProfileHandler.self] = newValue
}
}
}
Extend Environment
Environment Key
View modifier
struct AvatarEditProfileHandler: EnvironmentKey {
static var defaultValue: (() !$ Void)?
}
extension EnvironmentValues {
var editProfileHandler: (() !$ Void)? {
get { self[AvatarEditProfileHandler.self] }
set {
self[AvatarEditProfileHandler.self] = newValue
}
}
}
extension View {
public func onEditProfile(editProfileHandler:
@escaping () !$ Void) !$ some View {
environment(.editProfileHandler,
editProfileHandler)
}
}
Extend Environment
Environment Key
View modifier
Update view code
struct AvatarView: View {
@Environment(.avatarImageShape) var imageShape
!!#
}
}
}
var user: User
@ViewBuilder
var hero: some View { !!# }
var body: some View {
HStack(alignment: .top) {
hero
Extend Environment
Environment Key
View modifier
Update view code
struct AvatarView: View {
@Environment(.avatarImageShape) var imageShape
!!#
}
}
}
var user: User
@ViewBuilder
var hero: some View { !!# }
var body: some View {
HStack(alignment: .top) {
hero
@Environment(.editProfileHandler)
var editProfileHandler
Extend Environment
Environment Key
View modifier
Update view code
struct AvatarView: View {
@Environment(.avatarImageShape) var imageShape
var user: User
@ViewBuilder
var hero: some View { !!# }
var body: some View {
HStack(alignment: .top) {
hero
@Environment(.editProfileHandler)
var editProfileHandler
!!#
}
}
}
.onTapGesture {
if let editProfileHandler {
editProfileHandler()
}
}
Extend Environment
Environment Key
View modifier
Update view code
Usage at the call site
struct ContentView: View {
@State var isEditing = false
var body: some View {
AvatarView(user: User.sample)
.padding()
.onEditProfile {
isEditingProfile.toggle()
}
.sheet(isPresented: $isEditing) {
!!#
}
}
}
Techniques for composing views
Building a simple component
Configuring views
Styling views
Distributing views
Styling views
Styling views
View styles (Apple docs)
Styling SwiftUI Views (peterfriese.dev)
https://bit.ly/3DYKmyu
https://bit.ly/3qFbJKC
SwiftUI defines built-in styles for certain kinds of views and
automatically selects the appropriate style for a particular
presentation context. […]
You can override the automatic style by using one of the
style view modifiers. These modifiers typically propagate
throughout a container view, so that you can wrap a view
hierarchy in a style modifier to affect all the views of the
given type within the hierarchy.
“
Styling views
struct StylingExamples: View {
var body: some View {
Button("Unstyled button") { }
Button("Bordered button") { }
.buttonStyle(.bordered)
Button("Bordered prominent button") { }
.buttonStyle(.borderedProminent)
Button("Borderless button") { }
.buttonStyle(.borderless)
Button("Plain button") { }
.buttonStyle(.plain)
Button("Automatic button") { }
.buttonStyle(.automatic)
}
}
: Buttons
Styling views
struct ToggleStyleDemoView: View {
@State var isOn = true
var body: some View {
VStack {
Toggle(isOn: $isOn) {
Text("Custom toggle style")
}
.toggleStyle(.reminder)
Toggle(isOn: $isOn) {
Text("Default toggle style")
}
}
}
}
: a custom Toggle style
Style protocol
struct ReminderToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration)
!$ some View {
HStack {
Image(systemName: configuration.isOn
? "largecircle.fill.circle"
: "circle")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(configuration.isOn
? .accentColor
: .gray)
.onTapGesture {
configuration.isOn.toggle()
}
configuration.label
}
}
}
Style protocol
struct ReminderToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration)
!$ some View {
HStack {
Image(systemName: configuration.isOn
? "largecircle.fill.circle"
: "circle")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(configuration.isOn
? .accentColor
: .gray)
.onTapGesture {
configuration.isOn.toggle()
}
configuration.label
}
}
}
Style protocol
struct ReminderToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration)
!$ some View {
HStack {
Image(systemName: configuration.isOn
? "largecircle.fill.circle"
: "circle")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(configuration.isOn
? .accentColor
: .gray)
.onTapGesture {
configuration.isOn.toggle()
}
configuration.label
}
}
}
Style protocol
struct ReminderToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration)
!$ some View {
HStack {
Image(systemName: configuration.isOn
? "largecircle.fill.circle"
: "circle")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(configuration.isOn
? .accentColor
: .gray)
.onTapGesture {
configuration.isOn.toggle()
}
configuration.label
}
}
}
Define style shortcut
Style protocol
? .accentColor
: .gray)
.onTapGesture {
configuration.isOn.toggle()
}
configuration.label
}
}
}
extension ToggleStyle
where Self !% ReminderToggleStyle {
static var reminder: ReminderToggleStyle {
ReminderToggleStyle()
}
}
Define style shortcut
Style protocol
Apply the style
extension ToggleStyle
where Self !% ReminderToggleStyle {
static var reminder: ReminderToggleStyle {
ReminderToggleStyle()
}
}
struct ToggleStyleDemoView: View {
@State var isOn = true
var body: some View {
VStack {
Toggle(isOn: $isOn) {
Text("Custom toggle style")
}
.toggleStyle(.reminder)
}
}
}
Styling views
Audience of this talk
Styling views
View styles (Apple docs)
Styling SwiftUI Views (peterfriese.dev)
https://bit.ly/3DYKmyu
https://bit.ly/3qFbJKC
SwiftUI defines built-in styles for certain kinds of views and automatically
selects the appropriate style for a particular presentation context. […]
You can override the automatic style by using one of the style view
modifiers. These modifiers typically propagate throughout a container view,
so that you can wrap a view hierarchy in a style modifier to affect all the
views of the given type within the hierarchy.
“ Context:
* Platform
* Container
* Use case
Propagation:
* Design system
Create a configuration
struct AvatarStyleConfiguration {
let title: Title
struct Title: View {
let underlyingTitle: AnyView
init(_ title: some View) {
self.underlyingTitle = AnyView(title)
}
var body: some View {
underlyingTitle
}
}
Create a configuration
struct Title: View {
let underlyingTitle: AnyView
init(_ title: some View) {
self.underlyingTitle = AnyView(title)
}
var body: some View {
underlyingTitle
}
}
let subTitle: SubTitle
struct SubTitle: View {
let underlyingSubTitle: AnyView
init(_ subTitle: some View) {
self.underlyingSubTitle = AnyView(subTitle)
}
var body: some View {
underlyingSubTitle
}
}
Create a configuration
struct SubTitle: View {
let underlyingSubTitle: AnyView
init(_ subTitle: some View) {
self.underlyingSubTitle = AnyView(subTitle)
}
var body: some View {
underlyingSubTitle
}
}
let image: Image
init(title: Title,
subTitle: SubTitle,
image: Image) {
self.title = title
self.subTitle = subTitle
self.image = image
}
}
Define a style protocol
Create a configuration
let image: Image
init(title: Title,
subTitle: SubTitle,
image: Image) {
self.title = title
self.subTitle = subTitle
self.image = image
}
}
protocol AvatarStyle {
associatedtype Body: View
@ViewBuilder
func makeBody(configuration: Configuration)
!$ Body
typealias Configuration = AvatarStyleConfiguration
}
Define a style protocol
Create a configuration
Set up environment
protocol AvatarStyle {
associatedtype Body: View
@ViewBuilder
func makeBody(configuration: Configuration)
!$ Body
typealias Configuration = AvatarStyleConfiguration
}
struct AvatarStyleKey: EnvironmentKey {
static var defaultValue:
any AvatarStyle = DefaultAvatarStyle()
}
extension EnvironmentValues {
var avatarStyle: any AvatarStyle {
get { self[AvatarStyleKey.self] }
set { self[AvatarStyleKey.self] = newValue }
}
}
Define a style protocol
Create a configuration
Set up environment
struct AvatarStyleKey: EnvironmentKey {
static var defaultValue:
any AvatarStyle = DefaultAvatarStyle()
}
extension EnvironmentValues {
var avatarStyle: any AvatarStyle {
get { self[AvatarStyleKey.self] }
set { self[AvatarStyleKey.self] = newValue }
}
}
extension View {
func avatarStyle(_ style: some AvatarStyle)
!$ some View {
environment(.avatarStyle, style)
}
}
Define a style protocol
Create a configuration
Set up environment
Implement style
struct DefaultAvatarStyle: AvatarStyle {
func makeBody(configuration: Configuration)
!$ some View {
HStack(alignment: .top) {
configuration.image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50,
height: 50,
alignment: .center)
.clipShape(Circle(), style: FillStyle())
VStack(alignment: .leading) {
configuration.title
.font(.headline)
configuration.subTitle
.font(.subheadline)
}
Spacer()
}
}
}
Define a style protocol
Create a configuration
Set up environment
Implement style
Update view code
struct AvatarView: View {
@Environment(.avatarImageShape) var imageShape
@Environment(.editProfileHandler)
var editProfileHandler
@Environment(.avatarStyle) var style
var title: String
var subTitle: String
var imageName: String
init(_ title: String, subTitle: String,
image name: String) {
self.title = title
self.subTitle = subTitle
self.imageName = name
}
Define a style protocol
Create a configuration
Set up environment
Implement style
Update view code
var title: String
var subTitle: String
var imageName: String
init(_ title: String, subTitle: String,
image name: String) {
self.title = title
self.subTitle = subTitle
self.imageName = name
}
var body: some View {
let configuration = AvatarStyleConfiguration(
title: .init(Text(title)),
subTitle: .init(Text(subTitle)),
image: .init(imageName))
AnyView(
style.makeBody(configuration: configuration)
)
}
}
Define a style protocol
Create a configuration
Set up environment
Implement style
Update view code
var title: String
var subTitle: String
var imageName: String
init(_ title: String, subTitle: String,
image name: String) {
self.title = title
self.subTitle = subTitle
self.imageName = name
}
var body: some View {
let configuration = AvatarStyleConfiguration(
title: .init(Text(title)),
subTitle: .init(Text(subTitle)),
image: .init(imageName))
AnyView(
style.makeBody(configuration: configuration)
)
}
}
Define a style protocol
Create a configuration
Set up environment
Implement style
Update view code
Usage at the call site
struct ContentView: View {
@State var users = User.samples
@State var isProfileShowing = false
var body: some View {
List(users) { user in
AvatarView(user.fullName,
subTitle: user.affiliation,
image: user.profileImageName)
.avatarStyle(.automatic)
}
}
}
struct ContentView: View {
@State var users = User.samples
@State var isProfileShowing = false
var body: some View {
List(users) { user in
AvatarView(user.fullName,
subTitle: user.affiliation,
image: user.profileImageName)
.avatarStyle(.automatic)
}
}
}
Implement style
struct ProfileAvatarStyle: AvatarStyle {
func makeBody(configuration: Configuration)
!$ some View {
VStack(alignment: .center) {
configuration.image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 50,
height: 50,
alignment: .center)
.clipShape(Circle(), style: FillStyle())
configuration.title
.font(.headline)
configuration.subTitle
.font(.subheadline)
}
}
}
Define style shortcut
Implement style
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 50,
height: 50,
alignment: .center)
.clipShape(Circle(), style: FillStyle())
configuration.title
.font(.headline)
configuration.subTitle
.font(.subheadline)
}
}
}
extension AvatarStyle where Self !%
ProfileAvatarStyle {
static var profile: Self { .init() }
}
Define style shortcut
Implement style
Usage at the call site
AvatarView(user.fullName,
subTitle: user.affiliation,
image: user.profileImageName)
.avatarStyle(.profile)
AvatarView(user.fullName,
subTitle: user.affiliation,
image: user.profileImageName)
.avatarStyle(.profile)
Techniques for composing views
Building a simple component
Configuring views
Styling views
Distributing views
Distributing views
File > New Package
Add to the current
project
♻ Extract to
package
Dear Apple…
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Add Extract to File
Add Extract to local Subview
Add Extract to Package
Xcode Component Library
Register views, view modifiers, and styles
public struct AvatarView_Library: LibraryContentProvider {
public var views: [LibraryItem] {
[
LibraryItem(AvatarView("Peter", subTitle: "Google", image: ""),
title: "AvatarView",
category: .control)
]
}
public func modifiers(base: AvatarView) !$ [LibraryItem] {
[
LibraryItem(base.avatarStyle(.profile),
title: "Profile",
category: .control)
]
}
}
#
Dear Apple…
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Add Extract to File
Add Extract to local Subview
Add Extract to Package
Rich previews for the Xcode Component Library
Distributing
Resources
Source Code
YouTube series
https://bit.ly/3n99fis
https://bit.ly/building-swiftui-components
Composable Styles (Moving Parts) https://bit.ly/47w7wJY
Resources
Creating a Styleable Toggle
SwiftUI Components Tutorial @ iOSDevUK
https://bit.ly/3siupAk
September 4th - 7th, 2023
Thanks!
Thanks!
Peter Friese
peterfriese.dev
@peterfriese
youtube.com/c/PeterFriese/
not-only-swift.peterfriese.dev/
@peterfriese@iosdev.space

More Related Content

What's hot

Declarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeDeclarative UIs with Jetpack Compose
Declarative UIs with Jetpack Compose
Ramon Ribeiro Rabello
 
Demystifying Angular Animations
Demystifying Angular AnimationsDemystifying Angular Animations
Demystifying Angular Animations
Gil Fink
 
Design for succcess with react and storybook.js
Design for succcess with react and storybook.jsDesign for succcess with react and storybook.js
Design for succcess with react and storybook.js
Chris Saylor
 
SWTBot Tutorial
SWTBot TutorialSWTBot Tutorial
SWTBot Tutorial
Chris Aniszczyk
 
Jetpack Navigation Component
Jetpack Navigation ComponentJetpack Navigation Component
Jetpack Navigation Component
James Shvarts
 
Jetpack Compose a new way to implement UI on Android
Jetpack Compose a new way to implement UI on AndroidJetpack Compose a new way to implement UI on Android
Jetpack Compose a new way to implement UI on Android
Nelson Glauber Leal
 
Google Maps API
Google Maps APIGoogle Maps API
Google Maps API
M A Hossain Tonu
 
Better web apps with React and Redux
Better web apps with React and ReduxBetter web apps with React and Redux
Better web apps with React and Redux
Ali Sa'o
 
InjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdf
InjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdfInjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdf
InjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdf
정민 안
 
Introduction to thymeleaf
Introduction to thymeleafIntroduction to thymeleaf
Introduction to thymeleaf
NexThoughts Technologies
 
Try Jetpack Compose
Try Jetpack ComposeTry Jetpack Compose
Try Jetpack Compose
LutasLin
 
Android JetPack: easy navigation with the new Navigation Controller
Android JetPack: easy navigation with the new Navigation ControllerAndroid JetPack: easy navigation with the new Navigation Controller
Android JetPack: easy navigation with the new Navigation Controller
Leonardo Pirro
 
Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI
Ajinkya Saswade
 
Flutter state management from zero to hero
Flutter state management from zero to heroFlutter state management from zero to hero
Flutter state management from zero to hero
Ahmed Abu Eldahab
 
Advanced JavaScript
Advanced JavaScriptAdvanced JavaScript
Advanced JavaScript
Nascenia IT
 
Pengenalan ReactJS
Pengenalan ReactJS Pengenalan ReactJS
Pengenalan ReactJS
Fauzi Hasibuan
 
QA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшe
QA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшeQA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшe
QA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшe
QAFest
 
Getting Started with HTML5 in Tech Com (STC 2012)
Getting Started with HTML5 in Tech Com (STC 2012)Getting Started with HTML5 in Tech Com (STC 2012)
Getting Started with HTML5 in Tech Com (STC 2012)
Peter Lubbers
 
React入門
React入門React入門
React入門
GIG inc.
 
Jetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning TourJetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning Tour
Matthew Clarke
 

What's hot (20)

Declarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeDeclarative UIs with Jetpack Compose
Declarative UIs with Jetpack Compose
 
Demystifying Angular Animations
Demystifying Angular AnimationsDemystifying Angular Animations
Demystifying Angular Animations
 
Design for succcess with react and storybook.js
Design for succcess with react and storybook.jsDesign for succcess with react and storybook.js
Design for succcess with react and storybook.js
 
SWTBot Tutorial
SWTBot TutorialSWTBot Tutorial
SWTBot Tutorial
 
Jetpack Navigation Component
Jetpack Navigation ComponentJetpack Navigation Component
Jetpack Navigation Component
 
Jetpack Compose a new way to implement UI on Android
Jetpack Compose a new way to implement UI on AndroidJetpack Compose a new way to implement UI on Android
Jetpack Compose a new way to implement UI on Android
 
Google Maps API
Google Maps APIGoogle Maps API
Google Maps API
 
Better web apps with React and Redux
Better web apps with React and ReduxBetter web apps with React and Redux
Better web apps with React and Redux
 
InjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdf
InjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdfInjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdf
InjectionIII의 Hot Reload를 이용하여 앱 개발을 좀 더 편하게 하기.pdf
 
Introduction to thymeleaf
Introduction to thymeleafIntroduction to thymeleaf
Introduction to thymeleaf
 
Try Jetpack Compose
Try Jetpack ComposeTry Jetpack Compose
Try Jetpack Compose
 
Android JetPack: easy navigation with the new Navigation Controller
Android JetPack: easy navigation with the new Navigation ControllerAndroid JetPack: easy navigation with the new Navigation Controller
Android JetPack: easy navigation with the new Navigation Controller
 
Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI
 
Flutter state management from zero to hero
Flutter state management from zero to heroFlutter state management from zero to hero
Flutter state management from zero to hero
 
Advanced JavaScript
Advanced JavaScriptAdvanced JavaScript
Advanced JavaScript
 
Pengenalan ReactJS
Pengenalan ReactJS Pengenalan ReactJS
Pengenalan ReactJS
 
QA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшe
QA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшeQA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшe
QA Fes 2016. Алексей Виноградов. Page Objects: лучше проще, да лучшe
 
Getting Started with HTML5 in Tech Com (STC 2012)
Getting Started with HTML5 in Tech Com (STC 2012)Getting Started with HTML5 in Tech Com (STC 2012)
Getting Started with HTML5 in Tech Com (STC 2012)
 
React入門
React入門React入門
React入門
 
Jetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning TourJetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning Tour
 

Similar to Building Reusable SwiftUI Components

SwiftUI and Combine All the Things
SwiftUI and Combine All the ThingsSwiftUI and Combine All the Things
SwiftUI and Combine All the Things
Scott Gardner
 
Backbone.js
Backbone.jsBackbone.js
Backbone.js
Knoldus Inc.
 
Building Universal Web Apps with React ForwardJS 2017
Building Universal Web Apps with React ForwardJS 2017Building Universal Web Apps with React ForwardJS 2017
Building Universal Web Apps with React ForwardJS 2017
Elyse Kolker Gordon
 
Python Code Camp for Professionals 1/4
Python Code Camp for Professionals 1/4Python Code Camp for Professionals 1/4
Python Code Camp for Professionals 1/4
DEVCON
 
Android App Development - 04 Views and layouts
Android App Development - 04 Views and layoutsAndroid App Development - 04 Views and layouts
Android App Development - 04 Views and layouts
Diego Grancini
 
Vue routing tutorial getting started with vue router
Vue routing tutorial getting started with vue routerVue routing tutorial getting started with vue router
Vue routing tutorial getting started with vue router
Katy Slemon
 
Backbone js
Backbone jsBackbone js
Backbone js
Knoldus Inc.
 
WordPress as the Backbone(.js)
WordPress as the Backbone(.js)WordPress as the Backbone(.js)
WordPress as the Backbone(.js)
Beau Lebens
 
JavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
JavaCro'14 - Building interactive web applications with Vaadin – Peter LehtoJavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
JavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
HUJAK - Hrvatska udruga Java korisnika / Croatian Java User Association
 
How to create an Angular builder
How to create an Angular builderHow to create an Angular builder
How to create an Angular builder
Maurizio Vitale
 
Gutenberg sous le capot, modules réutilisables
Gutenberg sous le capot, modules réutilisablesGutenberg sous le capot, modules réutilisables
Gutenberg sous le capot, modules réutilisables
Riad Benguella
 
A Guide to Creating a Great Custom Tailwind Sidebar
A Guide to Creating a Great Custom Tailwind SidebarA Guide to Creating a Great Custom Tailwind Sidebar
A Guide to Creating a Great Custom Tailwind Sidebar
RonDosh
 
SciVerse Application Integration Points
SciVerse Application Integration PointsSciVerse Application Integration Points
SciVerse Application Integration PointsElsevier Developers
 
MVVM with SwiftUI and Combine
MVVM with SwiftUI and CombineMVVM with SwiftUI and Combine
MVVM with SwiftUI and Combine
Tai Lun Tseng
 
Writing HTML5 Web Apps using Backbone.js and GAE
Writing HTML5 Web Apps using Backbone.js and GAEWriting HTML5 Web Apps using Backbone.js and GAE
Writing HTML5 Web Apps using Backbone.js and GAE
Ron Reiter
 
Backbone.js — Introduction to client-side JavaScript MVC
Backbone.js — Introduction to client-side JavaScript MVCBackbone.js — Introduction to client-side JavaScript MVC
Backbone.js — Introduction to client-side JavaScript MVC
pootsbook
 
Rapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and FirebaseRapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and Firebase
Peter Friese
 
Android development for iOS developers
Android development for iOS developersAndroid development for iOS developers
Android development for iOS developers
Darryl Bayliss
 
Optimize CollectionView Scrolling
Optimize CollectionView ScrollingOptimize CollectionView Scrolling
Optimize CollectionView Scrolling
Andrea Prearo
 
Spring first in Magnolia CMS - Spring I/O 2015
Spring first in Magnolia CMS - Spring I/O 2015Spring first in Magnolia CMS - Spring I/O 2015
Spring first in Magnolia CMS - Spring I/O 2015Tobias Mattsson
 

Similar to Building Reusable SwiftUI Components (20)

SwiftUI and Combine All the Things
SwiftUI and Combine All the ThingsSwiftUI and Combine All the Things
SwiftUI and Combine All the Things
 
Backbone.js
Backbone.jsBackbone.js
Backbone.js
 
Building Universal Web Apps with React ForwardJS 2017
Building Universal Web Apps with React ForwardJS 2017Building Universal Web Apps with React ForwardJS 2017
Building Universal Web Apps with React ForwardJS 2017
 
Python Code Camp for Professionals 1/4
Python Code Camp for Professionals 1/4Python Code Camp for Professionals 1/4
Python Code Camp for Professionals 1/4
 
Android App Development - 04 Views and layouts
Android App Development - 04 Views and layoutsAndroid App Development - 04 Views and layouts
Android App Development - 04 Views and layouts
 
Vue routing tutorial getting started with vue router
Vue routing tutorial getting started with vue routerVue routing tutorial getting started with vue router
Vue routing tutorial getting started with vue router
 
Backbone js
Backbone jsBackbone js
Backbone js
 
WordPress as the Backbone(.js)
WordPress as the Backbone(.js)WordPress as the Backbone(.js)
WordPress as the Backbone(.js)
 
JavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
JavaCro'14 - Building interactive web applications with Vaadin – Peter LehtoJavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
JavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
 
How to create an Angular builder
How to create an Angular builderHow to create an Angular builder
How to create an Angular builder
 
Gutenberg sous le capot, modules réutilisables
Gutenberg sous le capot, modules réutilisablesGutenberg sous le capot, modules réutilisables
Gutenberg sous le capot, modules réutilisables
 
A Guide to Creating a Great Custom Tailwind Sidebar
A Guide to Creating a Great Custom Tailwind SidebarA Guide to Creating a Great Custom Tailwind Sidebar
A Guide to Creating a Great Custom Tailwind Sidebar
 
SciVerse Application Integration Points
SciVerse Application Integration PointsSciVerse Application Integration Points
SciVerse Application Integration Points
 
MVVM with SwiftUI and Combine
MVVM with SwiftUI and CombineMVVM with SwiftUI and Combine
MVVM with SwiftUI and Combine
 
Writing HTML5 Web Apps using Backbone.js and GAE
Writing HTML5 Web Apps using Backbone.js and GAEWriting HTML5 Web Apps using Backbone.js and GAE
Writing HTML5 Web Apps using Backbone.js and GAE
 
Backbone.js — Introduction to client-side JavaScript MVC
Backbone.js — Introduction to client-side JavaScript MVCBackbone.js — Introduction to client-side JavaScript MVC
Backbone.js — Introduction to client-side JavaScript MVC
 
Rapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and FirebaseRapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and Firebase
 
Android development for iOS developers
Android development for iOS developersAndroid development for iOS developers
Android development for iOS developers
 
Optimize CollectionView Scrolling
Optimize CollectionView ScrollingOptimize CollectionView Scrolling
Optimize CollectionView Scrolling
 
Spring first in Magnolia CMS - Spring I/O 2015
Spring first in Magnolia CMS - Spring I/O 2015Spring first in Magnolia CMS - Spring I/O 2015
Spring first in Magnolia CMS - Spring I/O 2015
 

More from Peter Friese

Firebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroesFirebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroes
Peter Friese
 
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
Peter Friese
 
async/await in Swift
async/await in Swiftasync/await in Swift
async/await in Swift
Peter Friese
 
Firebase for Apple Developers
Firebase for Apple DevelopersFirebase for Apple Developers
Firebase for Apple Developers
Peter Friese
 
Building Apps with SwiftUI and Firebase
Building Apps with SwiftUI and FirebaseBuilding Apps with SwiftUI and Firebase
Building Apps with SwiftUI and Firebase
Peter Friese
 
Rapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and FirebaseRapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and Firebase
Peter Friese
 
Five Things You Didn't Know About Firebase Auth
Five Things You Didn't Know About Firebase AuthFive Things You Didn't Know About Firebase Auth
Five Things You Didn't Know About Firebase Auth
Peter Friese
 
Building High-Quality Apps for Google Assistant
Building High-Quality Apps for Google AssistantBuilding High-Quality Apps for Google Assistant
Building High-Quality Apps for Google Assistant
Peter Friese
 
Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google
Peter Friese
 
Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on GoogleBuilding Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google
Peter Friese
 
What's new in Android Wear 2.0
What's new in Android Wear 2.0What's new in Android Wear 2.0
What's new in Android Wear 2.0
Peter Friese
 
Google Fit, Android Wear & Xamarin
Google Fit, Android Wear & XamarinGoogle Fit, Android Wear & Xamarin
Google Fit, Android Wear & Xamarin
Peter Friese
 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android Wear
Peter Friese
 
Google Play Services Rock
Google Play Services RockGoogle Play Services Rock
Google Play Services Rock
Peter Friese
 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android Wear
Peter Friese
 
Google+ for Mobile Apps on iOS and Android
Google+ for Mobile Apps on iOS and AndroidGoogle+ for Mobile Apps on iOS and Android
Google+ for Mobile Apps on iOS and Android
Peter Friese
 
Cross-Platform Authentication with Google+ Sign-In
Cross-Platform Authentication with Google+ Sign-InCross-Platform Authentication with Google+ Sign-In
Cross-Platform Authentication with Google+ Sign-In
Peter Friese
 
Bring Back the Fun to Testing Android Apps with Robolectric
Bring Back the Fun to Testing Android Apps with RobolectricBring Back the Fun to Testing Android Apps with Robolectric
Bring Back the Fun to Testing Android Apps with Robolectric
Peter Friese
 
Do Androids Dream of Electric Sheep
Do Androids Dream of Electric SheepDo Androids Dream of Electric Sheep
Do Androids Dream of Electric SheepPeter Friese
 
Java based Cross-Platform Mobile Development
Java based Cross-Platform Mobile DevelopmentJava based Cross-Platform Mobile Development
Java based Cross-Platform Mobile Development
Peter Friese
 

More from Peter Friese (20)

Firebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroesFirebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroes
 
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 
async/await in Swift
async/await in Swiftasync/await in Swift
async/await in Swift
 
Firebase for Apple Developers
Firebase for Apple DevelopersFirebase for Apple Developers
Firebase for Apple Developers
 
Building Apps with SwiftUI and Firebase
Building Apps with SwiftUI and FirebaseBuilding Apps with SwiftUI and Firebase
Building Apps with SwiftUI and Firebase
 
Rapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and FirebaseRapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and Firebase
 
Five Things You Didn't Know About Firebase Auth
Five Things You Didn't Know About Firebase AuthFive Things You Didn't Know About Firebase Auth
Five Things You Didn't Know About Firebase Auth
 
Building High-Quality Apps for Google Assistant
Building High-Quality Apps for Google AssistantBuilding High-Quality Apps for Google Assistant
Building High-Quality Apps for Google Assistant
 
Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google
 
Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on GoogleBuilding Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google
 
What's new in Android Wear 2.0
What's new in Android Wear 2.0What's new in Android Wear 2.0
What's new in Android Wear 2.0
 
Google Fit, Android Wear & Xamarin
Google Fit, Android Wear & XamarinGoogle Fit, Android Wear & Xamarin
Google Fit, Android Wear & Xamarin
 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android Wear
 
Google Play Services Rock
Google Play Services RockGoogle Play Services Rock
Google Play Services Rock
 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android Wear
 
Google+ for Mobile Apps on iOS and Android
Google+ for Mobile Apps on iOS and AndroidGoogle+ for Mobile Apps on iOS and Android
Google+ for Mobile Apps on iOS and Android
 
Cross-Platform Authentication with Google+ Sign-In
Cross-Platform Authentication with Google+ Sign-InCross-Platform Authentication with Google+ Sign-In
Cross-Platform Authentication with Google+ Sign-In
 
Bring Back the Fun to Testing Android Apps with Robolectric
Bring Back the Fun to Testing Android Apps with RobolectricBring Back the Fun to Testing Android Apps with Robolectric
Bring Back the Fun to Testing Android Apps with Robolectric
 
Do Androids Dream of Electric Sheep
Do Androids Dream of Electric SheepDo Androids Dream of Electric Sheep
Do Androids Dream of Electric Sheep
 
Java based Cross-Platform Mobile Development
Java based Cross-Platform Mobile DevelopmentJava based Cross-Platform Mobile Development
Java based Cross-Platform Mobile Development
 

Recently uploaded

Providing Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data AnalysisProviding Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data Analysis
Globus
 
GlobusWorld 2024 Opening Keynote session
GlobusWorld 2024 Opening Keynote sessionGlobusWorld 2024 Opening Keynote session
GlobusWorld 2024 Opening Keynote session
Globus
 
Corporate Management | Session 3 of 3 | Tendenci AMS
Corporate Management | Session 3 of 3 | Tendenci AMSCorporate Management | Session 3 of 3 | Tendenci AMS
Corporate Management | Session 3 of 3 | Tendenci AMS
Tendenci - The Open Source AMS (Association Management Software)
 
First Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User EndpointsFirst Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User Endpoints
Globus
 
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
Juraj Vysvader
 
OpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoam
OpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoamOpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoam
OpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoam
takuyayamamoto1800
 
SOCRadar Research Team: Latest Activities of IntelBroker
SOCRadar Research Team: Latest Activities of IntelBrokerSOCRadar Research Team: Latest Activities of IntelBroker
SOCRadar Research Team: Latest Activities of IntelBroker
SOCRadar
 
May Marketo Masterclass, London MUG May 22 2024.pdf
May Marketo Masterclass, London MUG May 22 2024.pdfMay Marketo Masterclass, London MUG May 22 2024.pdf
May Marketo Masterclass, London MUG May 22 2024.pdf
Adele Miller
 
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
Shahin Sheidaei
 
Enterprise Resource Planning System in Telangana
Enterprise Resource Planning System in TelanganaEnterprise Resource Planning System in Telangana
Enterprise Resource Planning System in Telangana
NYGGS Automation Suite
 
Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...
Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...
Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...
Mind IT Systems
 
Quarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden ExtensionsQuarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden Extensions
Max Andersen
 
Developing Distributed High-performance Computing Capabilities of an Open Sci...
Developing Distributed High-performance Computing Capabilities of an Open Sci...Developing Distributed High-performance Computing Capabilities of an Open Sci...
Developing Distributed High-performance Computing Capabilities of an Open Sci...
Globus
 
A Comprehensive Look at Generative AI in Retail App Testing.pdf
A Comprehensive Look at Generative AI in Retail App Testing.pdfA Comprehensive Look at Generative AI in Retail App Testing.pdf
A Comprehensive Look at Generative AI in Retail App Testing.pdf
kalichargn70th171
 
Large Language Models and the End of Programming
Large Language Models and the End of ProgrammingLarge Language Models and the End of Programming
Large Language Models and the End of Programming
Matt Welsh
 
top nidhi software solution freedownload
top nidhi software solution freedownloadtop nidhi software solution freedownload
top nidhi software solution freedownload
vrstrong314
 
Globus Compute wth IRI Workflows - GlobusWorld 2024
Globus Compute wth IRI Workflows - GlobusWorld 2024Globus Compute wth IRI Workflows - GlobusWorld 2024
Globus Compute wth IRI Workflows - GlobusWorld 2024
Globus
 
Top Features to Include in Your Winzo Clone App for Business Growth (4).pptx
Top Features to Include in Your Winzo Clone App for Business Growth (4).pptxTop Features to Include in Your Winzo Clone App for Business Growth (4).pptx
Top Features to Include in Your Winzo Clone App for Business Growth (4).pptx
rickgrimesss22
 
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Globus
 
Vitthal Shirke Microservices Resume Montevideo
Vitthal Shirke Microservices Resume MontevideoVitthal Shirke Microservices Resume Montevideo
Vitthal Shirke Microservices Resume Montevideo
Vitthal Shirke
 

Recently uploaded (20)

Providing Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data AnalysisProviding Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data Analysis
 
GlobusWorld 2024 Opening Keynote session
GlobusWorld 2024 Opening Keynote sessionGlobusWorld 2024 Opening Keynote session
GlobusWorld 2024 Opening Keynote session
 
Corporate Management | Session 3 of 3 | Tendenci AMS
Corporate Management | Session 3 of 3 | Tendenci AMSCorporate Management | Session 3 of 3 | Tendenci AMS
Corporate Management | Session 3 of 3 | Tendenci AMS
 
First Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User EndpointsFirst Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User Endpoints
 
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
 
OpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoam
OpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoamOpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoam
OpenFOAM solver for Helmholtz equation, helmholtzFoam / helmholtzBubbleFoam
 
SOCRadar Research Team: Latest Activities of IntelBroker
SOCRadar Research Team: Latest Activities of IntelBrokerSOCRadar Research Team: Latest Activities of IntelBroker
SOCRadar Research Team: Latest Activities of IntelBroker
 
May Marketo Masterclass, London MUG May 22 2024.pdf
May Marketo Masterclass, London MUG May 22 2024.pdfMay Marketo Masterclass, London MUG May 22 2024.pdf
May Marketo Masterclass, London MUG May 22 2024.pdf
 
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
 
Enterprise Resource Planning System in Telangana
Enterprise Resource Planning System in TelanganaEnterprise Resource Planning System in Telangana
Enterprise Resource Planning System in Telangana
 
Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...
Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...
Custom Healthcare Software for Managing Chronic Conditions and Remote Patient...
 
Quarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden ExtensionsQuarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden Extensions
 
Developing Distributed High-performance Computing Capabilities of an Open Sci...
Developing Distributed High-performance Computing Capabilities of an Open Sci...Developing Distributed High-performance Computing Capabilities of an Open Sci...
Developing Distributed High-performance Computing Capabilities of an Open Sci...
 
A Comprehensive Look at Generative AI in Retail App Testing.pdf
A Comprehensive Look at Generative AI in Retail App Testing.pdfA Comprehensive Look at Generative AI in Retail App Testing.pdf
A Comprehensive Look at Generative AI in Retail App Testing.pdf
 
Large Language Models and the End of Programming
Large Language Models and the End of ProgrammingLarge Language Models and the End of Programming
Large Language Models and the End of Programming
 
top nidhi software solution freedownload
top nidhi software solution freedownloadtop nidhi software solution freedownload
top nidhi software solution freedownload
 
Globus Compute wth IRI Workflows - GlobusWorld 2024
Globus Compute wth IRI Workflows - GlobusWorld 2024Globus Compute wth IRI Workflows - GlobusWorld 2024
Globus Compute wth IRI Workflows - GlobusWorld 2024
 
Top Features to Include in Your Winzo Clone App for Business Growth (4).pptx
Top Features to Include in Your Winzo Clone App for Business Growth (4).pptxTop Features to Include in Your Winzo Clone App for Business Growth (4).pptx
Top Features to Include in Your Winzo Clone App for Business Growth (4).pptx
 
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
 
Vitthal Shirke Microservices Resume Montevideo
Vitthal Shirke Microservices Resume MontevideoVitthal Shirke Microservices Resume Montevideo
Vitthal Shirke Microservices Resume Montevideo
 

Building Reusable SwiftUI Components

  • 1. Peter Friese, Developer Relations Engineer, Google Building Reusable SwiftUI Components @peterfriese SwiftConf 2023,Cologne
  • 2. Peter Friese, Developer Relations Engineer, Google Building Reusable SwiftUI Components @peterfriese SwiftConf 2023,Cologne
  • 4. Peter Friese, Developer Relations Engineer, Google Building Reusable SwiftUI Components SwiftConf 2023,Cologne Why this talk? @peterfriese
  • 5.
  • 6.
  • 7.
  • 8. Help developers succeed by making it easy to build and grow apps
  • 11.
  • 13. Techniques for composing views Building a simple component Configuring views Styling views Distributing views
  • 14. Building a simple component
  • 15. Building a simple component app
  • 16. import SwiftUI struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() } }
  • 17. import SwiftUI struct ContentView: View { var body: some View { HStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() } }
  • 18. import SwiftUI struct ContentView: View { var body: some View { List(0 !!" 5) { item in HStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() } } }
  • 19. import SwiftUI struct ContentView: View { var body: some View { List(0 !!" 5) { item in HStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } } } }
  • 20. import SwiftUI struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in HStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text(user.fullName) } } } }
  • 21. import SwiftUI struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in HStack { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) Text(user.fullName) } } } }
  • 22. import SwiftUI struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in HStack { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack { Text(user.fullName) Text(user.affiliation) } } } } }
  • 23. import SwiftUI struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) Text(user.affiliation) } } } } }
  • 24. import SwiftUI struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } } } }
  • 25.
  • 26. Move to file Extract Subview Extract to local subview (property) Extract to local subview (function)
  • 27. Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } } } } ♻ Extract Subview
  • 28. Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in ExtractedView() } } } struct ExtractedView: View { var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation)
  • 29. Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in ExtractedView() } } } struct ExtractedView: View { var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation)
  • 30. Dear Apple… Make Extract to Subview work all of the time
  • 31. Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in ExtractedView() } } } struct ExtractedView: View { var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) ♻ Rename
  • 32. Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView() } } } struct AvatarView: View { var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation)
  • 33. ❌ Cannot find ‘user’ in scope ❌ Cannot find ‘user’ in scope ❌ Cannot find ‘user’ in scope Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView() } } } struct AvatarView: View { var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation)
  • 34. ❌ Cannot find ‘user’ in scope ❌ Cannot find ‘user’ in scope Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView() } } } struct AvatarView: View { var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) var user: User
  • 35. ❌ Cannot find ‘user’ in scope ❌ Cannot find ‘user’ in scope Extract Subview struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(user: user) } } } struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline)
  • 36. Dear Apple… Make Extract to Subview work all of the time Extract to Subview: handle dependent properties
  • 37. Move to file Extract Subview Extract to local subview (property) Extract to local subview (function)
  • 38. Move to file struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(user: user) } } } struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) ♻ Move to file
  • 39. Move to file New SwiftUI File
  • 40. Move to file Make sure to use type name
  • 41. Move to file import SwiftUI struct AvatarView: View { var body: some View { Text("Hello, World!") } } #Preview { AvatarView() }
  • 42. ❌ Missing argument for parameter 'user' in call Move to file struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } } } #Preview { AvatarView() Need to fix the preview
  • 43. ❌ Missing argument for parameter 'user' in call Move to file struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } } } #Preview { AvatarView(user: User.sample)
  • 44. Dear Apple… Make Extract to Subview work all of the time Extract to Subview: handle dependent properties Add Extract to File
  • 45. struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } } } #Preview { AvatarView(user: User.sample) } What’s this?!
  • 46. struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } } } #Preview { AvatarView(user: User.sample) }
  • 47. SwiftUI Layout Behaviour Building layouts with stack views https://apple.co/448CaWL Created by redemption_art from the Noun Project Created by redemption_art from the Noun Project Expanding Hugging
  • 48. HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } Created by redemption_art from the Noun Project Text is a content-hugging view
  • 49. HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } Created by redemption_art from the Noun Project VStack is a content-hugging view
  • 50. HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } Image is an expanding view Created by redemption_art from the Noun Project … but we explicitly constrained it
  • 51. HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } HStack is a content-hugging view Created by redemption_art from the Noun Project
  • 52. HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } Spacer() } Spacer is an expanding view Created by redemption_art from the Noun Project
  • 53. Move to file Extract Subview Extract to local subview (property) Extract to local subview (function)
  • 54. Extract to local subview (property) struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } Spacer() } } } ♻ Extract to local subview
  • 55. Extract to local subview (property) struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.affiliation) .font(.subheadline) } var titleLabel: some View { } Text(user.fullName) .font(.headline)
  • 56. Extract to local subview (property) struct AvatarView: View { var user: User var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.affiliation) .font(.subheadline) } var titleLabel: some View { } Text(user.fullName) .font(.headline)
  • 57. Extract to local subview (property) struct AvatarView: View { var user: User var titleLabel: some View { Text(user.fullName) .font(.headline) } var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.affiliation) .font(.subheadline) } Spacer() titleLabel
  • 58. Dear Apple… Make Extract to Subview work all of the time Extract to Subview: handle dependent properties Add Extract to File Add Extract to local Subview
  • 59. Move to file Extract Subview Extract to local subview (property) Extract to local subview (function)
  • 60. Extract to local subview (function) var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { titleLabel Text("(Image(systemName: "graduationcap")) (user.jobtitle)") .font(.subheadline) Text("(Image(systemName: "building.2")) (user.affiliation)") .font(.subheadline) } Spacer() } } struct AvatarView: View { var user: User var titleLabel: some View {!!#}
  • 61. Extract to local subview (function) struct AvatarView: View { var user: User var titleLabel: some View {!!#} var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { titleLabel Text("(Image(systemName: "graduationcap")) (user.jobtitle)") .font(.subheadline) Text("(Image(systemName: "building.2")) (user.affiliation)") func detailsLabel(_ text: String, systemName: String) !$ some View { }
  • 62. Extract to local subview (function) struct AvatarView: View { var user: User var titleLabel: some View {!!#} var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { titleLabel Text("(Image(systemName: "building.2")) (user.affiliation)") func detailsLabel(_ text: String, systemName: String) !$ some View { } Text("(Image(systemName: "graduationcap")) (user.jobtitle)") .font(.subheadline)
  • 63. Extract to local subview (function) struct AvatarView: View { var user: User var titleLabel: some View {!!#} var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { titleLabel Text("(Image(systemName: "building.2")) (user.affiliation)") func detailsLabel(_ text: String, systemName: String) !$ some View { } Text("(Image(systemName: "graduationcap")) (user.jobtitle)") .font(.subheadline)
  • 64. Extract to local subview (function) struct AvatarView: View { var user: User var titleLabel: some View {!!#} var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { titleLabel Text("(Im func detailsLabel(_ text: String, systemName: String) !$ some View { } Text("(Image(systemName: .font(.subheadline) "graduationcap")) (user.jobtitle)") "(systemName)")) (text)") Text("(Image(systemName: "building.2")) (user.affiliation)")
  • 65. Extract to local subview (function) struct AvatarView: View { var user: User var titleLabel: some View {!!#} func detailsLabel(_ text: String, systemName: String) !$ some View { Text("(Image(systemName: "(systemName)")) (text)") .font(.subheadline) } var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { titleLabel Text("(Image(systemName: "building.2")) (user.affiliation)") .font(.subheadline) detailsLabel(user.jobtitle, systemName: "graduationcap") detailsLabel(user.affiliation, systemName: "building.2")
  • 66. Extract to local subview (function) struct AvatarView: View { var user: User var titleLabel: some View {!!#} func detailsLabel(_ text: String, systemName: String) !$ some View { Text("(Image(systemName: "(systemName)")) (text)") .font(.subheadline) } var body: some View { HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { titleLabel detailsLabel(user.jobtitle, systemName: "graduationcap") detailsLabel(user.affiliation, systemName: "building.2") }
  • 67. Techniques for composing views Building a simple component Configuring views Styling views Distributing views
  • 69. View body import SwiftUI struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in HStack(alignment: .top) { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { Text(user.fullName) .font(.headline) Text(user.affiliation) .font(.subheadline) } } } } }
  • 70. Local properties View body struct AvatarView: View { var user: User var titleLabel: some View { Text(user.fullName) .font(.headline) } var body: some View { !!# titleLabel !!# } }
  • 71. Local properties View body Local functions struct AvatarView: View { var user: User func detailsLabel(_ text: String, systemName: String) !$ some View { Text("(Image(systemName: "(systemName)")) (text)") .font(.subheadline) } var body: some View { !!# detailsLabel(user.jobtitle, systemName: "graduationcap") detailsLabel(user.affiliation, systemName: “building.2") !!# } }
  • 72. Local properties View body Local functions View Builders
  • 73. View Builders @ViewBuilder usage explained with code examples (SwiftLee) https://bit.ly/44bp37t The @ViewBuilder attribute is one of the few result builders available for you to use in SwiftUI. You typically use it to create child views for a specific SwiftUI view in a readable way without having to use any return keywords. “
  • 74. View Builders struct AvatarView: View { var user: User var titleLabel: some View { Text(user.fullName) .font(.headline) } func detailsLabel(_ text: String, systemName: String) !$ some View { Text("(Image(systemName: "(systemName)")) (text)") .font(.subheadline) } var body: some View { !!# detailsLabel(user.jobtitle, systemName: "graduationcap") detailsLabel(user.affiliation, systemName: “building.2") !!# } Why no @ViewBuilder?
  • 75. View Builders struct AvatarView: View { var user: User var hero: some View { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } var body: some View { HStack(alignment: .top) { hero VStack(alignment: .leading) {!!# } } } }
  • 76. View Builders struct AvatarView: View { var isRound = true var user: User var hero: some View { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } var body: some View { HStack(alignment: .top) { hero VStack(alignment: .leading) {!!# } } } }
  • 77. ❌ Branches have mismatching types 'some View' View Builders struct AvatarView: View { var isRound = true var user: User var hero: some View { if isRound { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } else { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) } } var body: some View { Self.clipShape(_:style:) Self.frame(width:height:alignment:)
  • 78. View Builders struct AvatarView: View { var isRound = true var user: User var hero: some View { if isRound { AnyView { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } } else { AnyView { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) } AnyView erases the view’s type information
  • 79. View Builders struct AvatarView: View { var isRound = true var user: User var hero: some View { if isRound { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } else { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) } } @ViewBuilder @ViewBuilder keeps the type information
  • 80. View Builders How to avoid using AnyView in SwiftUI (Tanaschita.com) ViewBuilder vs. AnyView (Alexito's World) https://bit.ly/45kVOQI https://bit.ly/3OUbViE Use @ViewBuilder if you want to return structurally different views from a property / function. “
  • 81. Techniques for composing views Building a simple component Configuring views Styling views Distributing views
  • 83. Configuring views struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(isRound: true, user: user) } } } Let’s turn this into a view modifier : properties
  • 84. Enum for the shape enum AvatarImageShape { case round case rectangle }
  • 85. Environment Key Enum for the shape struct AvatarImageShapeKey: EnvironmentKey { static var defaultValue: AvatarImageShape = .round } enum AvatarImageShape { case round case rectangle }
  • 86. Environment Key Enum for the shape Extend Environment struct AvatarImageShapeKey: EnvironmentKey { static var defaultValue: AvatarImageShape = .round } enum AvatarImageShape { case round case rectangle } extension EnvironmentValues { var avatarImageShape: AvatarImageShape { get { self[AvatarImageShapeKey.self] } set { self[AvatarImageShapeKey.self] = newValue } } }
  • 87. Environment Key Enum for the shape Extend Environment View modifier struct AvatarImageShapeKey: EnvironmentKey { static var defaultValue: AvatarImageShape = .round } enum AvatarImageShape { case round case rectangle } extension EnvironmentValues { var avatarImageShape: AvatarImageShape { get { self[AvatarImageShapeKey.self] } set { self[AvatarImageShapeKey.self] = newValue } } } extension View { func avatarImageShape(_ imageShape: AvatarImageShape) !$ some View { environment(.avatarImageShape, imageShape) } }
  • 88. Environment Key Enum for the shape Extend Environment View modifier Update view code struct AvatarView: View { var isRound = true var user: User @ViewBuilder var hero: some View { if isRound { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } else { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) } } !!# } var isRound = true
  • 89. Environment Key Enum for the shape Extend Environment View modifier Update view code struct AvatarView: View { var user: User @ViewBuilder var hero: some View { if isRound { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } else { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) } } !!# } var isRound = true @Environment(.avatarImageShape) var imageShape
  • 90. Environment Key Enum for the shape Extend Environment View modifier Update view code struct AvatarView: View { var user: User @ViewBuilder var hero: some View { if { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) .clipShape(Circle(), style: FillStyle()) } else { Image(user.profileImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 75) } } !!# } @Environment(.avatarImageShape) var imageShape imageShape !% .round
  • 91. Environment Key Enum for the shape Extend Environment View modifier Update view code Usage at the call site struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(user: user) .avatarImageShape(.rectangle) } } }
  • 92. Environment Key Enum for the shape Extend Environment View modifier Update view code Usage at the call site struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(user: user) .avatarImageShape(.rectangle) } } } struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(user: user) } .avatarImageShape(.rectangle) } }
  • 93. Environment Key Enum for the shape Extend Environment View modifier Update view code Usage at the call site struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(user: user) } .avatarImageShape(.rectangle) } } struct ContentView: View { @State var users = User.samples var body: some View { List(users) { user in AvatarView(user: user) .avatarImageShape( user.isTalking ? .round : .rectangle) } .avatarImageShape(.rectangle) } }
  • 94. Configuring views struct ContentView: View { var body: some View { AvatarView(user: User.sample) .onEditProfile { print("onEditProfile triggered") } } } Let’s register an action handler : action handlers
  • 95. Environment Key struct AvatarEditProfileHandler: EnvironmentKey { static var defaultValue: (() !$ Void)? }
  • 96. Extend Environment Environment Key struct AvatarEditProfileHandler: EnvironmentKey { static var defaultValue: (() !$ Void)? } extension EnvironmentValues { var editProfileHandler: (() !$ Void)? { get { self[AvatarEditProfileHandler.self] } set { self[AvatarEditProfileHandler.self] = newValue } } }
  • 97. Extend Environment Environment Key View modifier struct AvatarEditProfileHandler: EnvironmentKey { static var defaultValue: (() !$ Void)? } extension EnvironmentValues { var editProfileHandler: (() !$ Void)? { get { self[AvatarEditProfileHandler.self] } set { self[AvatarEditProfileHandler.self] = newValue } } } extension View { public func onEditProfile(editProfileHandler: @escaping () !$ Void) !$ some View { environment(.editProfileHandler, editProfileHandler) } }
  • 98. Extend Environment Environment Key View modifier Update view code struct AvatarView: View { @Environment(.avatarImageShape) var imageShape !!# } } } var user: User @ViewBuilder var hero: some View { !!# } var body: some View { HStack(alignment: .top) { hero
  • 99. Extend Environment Environment Key View modifier Update view code struct AvatarView: View { @Environment(.avatarImageShape) var imageShape !!# } } } var user: User @ViewBuilder var hero: some View { !!# } var body: some View { HStack(alignment: .top) { hero @Environment(.editProfileHandler) var editProfileHandler
  • 100. Extend Environment Environment Key View modifier Update view code struct AvatarView: View { @Environment(.avatarImageShape) var imageShape var user: User @ViewBuilder var hero: some View { !!# } var body: some View { HStack(alignment: .top) { hero @Environment(.editProfileHandler) var editProfileHandler !!# } } } .onTapGesture { if let editProfileHandler { editProfileHandler() } }
  • 101. Extend Environment Environment Key View modifier Update view code Usage at the call site struct ContentView: View { @State var isEditing = false var body: some View { AvatarView(user: User.sample) .padding() .onEditProfile { isEditingProfile.toggle() } .sheet(isPresented: $isEditing) { !!# } } }
  • 102. Techniques for composing views Building a simple component Configuring views Styling views Distributing views
  • 104. Styling views View styles (Apple docs) Styling SwiftUI Views (peterfriese.dev) https://bit.ly/3DYKmyu https://bit.ly/3qFbJKC SwiftUI defines built-in styles for certain kinds of views and automatically selects the appropriate style for a particular presentation context. […] You can override the automatic style by using one of the style view modifiers. These modifiers typically propagate throughout a container view, so that you can wrap a view hierarchy in a style modifier to affect all the views of the given type within the hierarchy. “
  • 105. Styling views struct StylingExamples: View { var body: some View { Button("Unstyled button") { } Button("Bordered button") { } .buttonStyle(.bordered) Button("Bordered prominent button") { } .buttonStyle(.borderedProminent) Button("Borderless button") { } .buttonStyle(.borderless) Button("Plain button") { } .buttonStyle(.plain) Button("Automatic button") { } .buttonStyle(.automatic) } } : Buttons
  • 106. Styling views struct ToggleStyleDemoView: View { @State var isOn = true var body: some View { VStack { Toggle(isOn: $isOn) { Text("Custom toggle style") } .toggleStyle(.reminder) Toggle(isOn: $isOn) { Text("Default toggle style") } } } } : a custom Toggle style
  • 107. Style protocol struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) !$ some View { HStack { Image(systemName: configuration.isOn ? "largecircle.fill.circle" : "circle") .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  • 108. Style protocol struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) !$ some View { HStack { Image(systemName: configuration.isOn ? "largecircle.fill.circle" : "circle") .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  • 109. Style protocol struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) !$ some View { HStack { Image(systemName: configuration.isOn ? "largecircle.fill.circle" : "circle") .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  • 110. Style protocol struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) !$ some View { HStack { Image(systemName: configuration.isOn ? "largecircle.fill.circle" : "circle") .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  • 111. Define style shortcut Style protocol ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } } extension ToggleStyle where Self !% ReminderToggleStyle { static var reminder: ReminderToggleStyle { ReminderToggleStyle() } }
  • 112. Define style shortcut Style protocol Apply the style extension ToggleStyle where Self !% ReminderToggleStyle { static var reminder: ReminderToggleStyle { ReminderToggleStyle() } } struct ToggleStyleDemoView: View { @State var isOn = true var body: some View { VStack { Toggle(isOn: $isOn) { Text("Custom toggle style") } .toggleStyle(.reminder) } } }
  • 114. Styling views View styles (Apple docs) Styling SwiftUI Views (peterfriese.dev) https://bit.ly/3DYKmyu https://bit.ly/3qFbJKC SwiftUI defines built-in styles for certain kinds of views and automatically selects the appropriate style for a particular presentation context. […] You can override the automatic style by using one of the style view modifiers. These modifiers typically propagate throughout a container view, so that you can wrap a view hierarchy in a style modifier to affect all the views of the given type within the hierarchy. “ Context: * Platform * Container * Use case Propagation: * Design system
  • 115.
  • 116. Create a configuration struct AvatarStyleConfiguration { let title: Title struct Title: View { let underlyingTitle: AnyView init(_ title: some View) { self.underlyingTitle = AnyView(title) } var body: some View { underlyingTitle } }
  • 117. Create a configuration struct Title: View { let underlyingTitle: AnyView init(_ title: some View) { self.underlyingTitle = AnyView(title) } var body: some View { underlyingTitle } } let subTitle: SubTitle struct SubTitle: View { let underlyingSubTitle: AnyView init(_ subTitle: some View) { self.underlyingSubTitle = AnyView(subTitle) } var body: some View { underlyingSubTitle } }
  • 118. Create a configuration struct SubTitle: View { let underlyingSubTitle: AnyView init(_ subTitle: some View) { self.underlyingSubTitle = AnyView(subTitle) } var body: some View { underlyingSubTitle } } let image: Image init(title: Title, subTitle: SubTitle, image: Image) { self.title = title self.subTitle = subTitle self.image = image } }
  • 119. Define a style protocol Create a configuration let image: Image init(title: Title, subTitle: SubTitle, image: Image) { self.title = title self.subTitle = subTitle self.image = image } } protocol AvatarStyle { associatedtype Body: View @ViewBuilder func makeBody(configuration: Configuration) !$ Body typealias Configuration = AvatarStyleConfiguration }
  • 120. Define a style protocol Create a configuration Set up environment protocol AvatarStyle { associatedtype Body: View @ViewBuilder func makeBody(configuration: Configuration) !$ Body typealias Configuration = AvatarStyleConfiguration } struct AvatarStyleKey: EnvironmentKey { static var defaultValue: any AvatarStyle = DefaultAvatarStyle() } extension EnvironmentValues { var avatarStyle: any AvatarStyle { get { self[AvatarStyleKey.self] } set { self[AvatarStyleKey.self] = newValue } } }
  • 121. Define a style protocol Create a configuration Set up environment struct AvatarStyleKey: EnvironmentKey { static var defaultValue: any AvatarStyle = DefaultAvatarStyle() } extension EnvironmentValues { var avatarStyle: any AvatarStyle { get { self[AvatarStyleKey.self] } set { self[AvatarStyleKey.self] = newValue } } } extension View { func avatarStyle(_ style: some AvatarStyle) !$ some View { environment(.avatarStyle, style) } }
  • 122. Define a style protocol Create a configuration Set up environment Implement style struct DefaultAvatarStyle: AvatarStyle { func makeBody(configuration: Configuration) !$ some View { HStack(alignment: .top) { configuration.image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 50, height: 50, alignment: .center) .clipShape(Circle(), style: FillStyle()) VStack(alignment: .leading) { configuration.title .font(.headline) configuration.subTitle .font(.subheadline) } Spacer() } } }
  • 123. Define a style protocol Create a configuration Set up environment Implement style Update view code struct AvatarView: View { @Environment(.avatarImageShape) var imageShape @Environment(.editProfileHandler) var editProfileHandler @Environment(.avatarStyle) var style var title: String var subTitle: String var imageName: String init(_ title: String, subTitle: String, image name: String) { self.title = title self.subTitle = subTitle self.imageName = name }
  • 124. Define a style protocol Create a configuration Set up environment Implement style Update view code var title: String var subTitle: String var imageName: String init(_ title: String, subTitle: String, image name: String) { self.title = title self.subTitle = subTitle self.imageName = name } var body: some View { let configuration = AvatarStyleConfiguration( title: .init(Text(title)), subTitle: .init(Text(subTitle)), image: .init(imageName)) AnyView( style.makeBody(configuration: configuration) ) } }
  • 125. Define a style protocol Create a configuration Set up environment Implement style Update view code var title: String var subTitle: String var imageName: String init(_ title: String, subTitle: String, image name: String) { self.title = title self.subTitle = subTitle self.imageName = name } var body: some View { let configuration = AvatarStyleConfiguration( title: .init(Text(title)), subTitle: .init(Text(subTitle)), image: .init(imageName)) AnyView( style.makeBody(configuration: configuration) ) } }
  • 126. Define a style protocol Create a configuration Set up environment Implement style Update view code Usage at the call site struct ContentView: View { @State var users = User.samples @State var isProfileShowing = false var body: some View { List(users) { user in AvatarView(user.fullName, subTitle: user.affiliation, image: user.profileImageName) .avatarStyle(.automatic) } } }
  • 127. struct ContentView: View { @State var users = User.samples @State var isProfileShowing = false var body: some View { List(users) { user in AvatarView(user.fullName, subTitle: user.affiliation, image: user.profileImageName) .avatarStyle(.automatic) } } }
  • 128.
  • 129.
  • 130. Implement style struct ProfileAvatarStyle: AvatarStyle { func makeBody(configuration: Configuration) !$ some View { VStack(alignment: .center) { configuration.image .resizable() .aspectRatio(contentMode: .fill) .frame(width: 50, height: 50, alignment: .center) .clipShape(Circle(), style: FillStyle()) configuration.title .font(.headline) configuration.subTitle .font(.subheadline) } } }
  • 131. Define style shortcut Implement style .resizable() .aspectRatio(contentMode: .fill) .frame(width: 50, height: 50, alignment: .center) .clipShape(Circle(), style: FillStyle()) configuration.title .font(.headline) configuration.subTitle .font(.subheadline) } } } extension AvatarStyle where Self !% ProfileAvatarStyle { static var profile: Self { .init() } }
  • 132. Define style shortcut Implement style Usage at the call site AvatarView(user.fullName, subTitle: user.affiliation, image: user.profileImageName) .avatarStyle(.profile)
  • 134. Techniques for composing views Building a simple component Configuring views Styling views Distributing views
  • 136. File > New Package Add to the current project
  • 138. Dear Apple… Make Extract to Subview work all of the time Extract to Subview: handle dependent properties Add Extract to File Add Extract to local Subview Add Extract to Package
  • 140.
  • 141. Register views, view modifiers, and styles public struct AvatarView_Library: LibraryContentProvider { public var views: [LibraryItem] { [ LibraryItem(AvatarView("Peter", subTitle: "Google", image: ""), title: "AvatarView", category: .control) ] } public func modifiers(base: AvatarView) !$ [LibraryItem] { [ LibraryItem(base.avatarStyle(.profile), title: "Profile", category: .control) ] } }
  • 142. #
  • 143. Dear Apple… Make Extract to Subview work all of the time Extract to Subview: handle dependent properties Add Extract to File Add Extract to local Subview Add Extract to Package Rich previews for the Xcode Component Library
  • 145.
  • 146.
  • 147.
  • 149. Resources Creating a Styleable Toggle SwiftUI Components Tutorial @ iOSDevUK https://bit.ly/3siupAk September 4th - 7th, 2023