SlideShare a Scribd company logo
1 of 151
Download to read offline
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

Build RESTful API Using Express JS
Build RESTful API Using Express JSBuild RESTful API Using Express JS
Build RESTful API Using Express JSCakra Danu Sedayu
Ā 
Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...
Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...
Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...Edureka!
Ā 
Jetpack Compose - Androidā€™s modern toolkit for building native UI
Jetpack Compose - Androidā€™s modern toolkit for building native UIJetpack Compose - Androidā€™s modern toolkit for building native UI
Jetpack Compose - Androidā€™s modern toolkit for building native UIGilang Ramadhan
Ā 
Spring boot - an introduction
Spring boot - an introductionSpring boot - an introduction
Spring boot - an introductionJonathan Holloway
Ā 
Spring - Part 1 - IoC, Di and Beans
Spring - Part 1 - IoC, Di and Beans Spring - Part 1 - IoC, Di and Beans
Spring - Part 1 - IoC, Di and Beans Hitesh-Java
Ā 
Deep dive into swift UI
Deep dive into swift UIDeep dive into swift UI
Deep dive into swift UIOsamaGamal26
Ā 
REST APIs with Spring
REST APIs with SpringREST APIs with Spring
REST APIs with SpringJoshua Long
Ā 
Swift UI - Declarative Programming [Pramati Technologies]
Swift UI - Declarative Programming [Pramati Technologies]Swift UI - Declarative Programming [Pramati Technologies]
Swift UI - Declarative Programming [Pramati Technologies]Pramati Technologies
Ā 
Threading Made Easy! A Busy Developerā€™s Guide to Kotlin Coroutines
Threading Made Easy! A Busy Developerā€™s Guide to Kotlin CoroutinesThreading Made Easy! A Busy Developerā€™s Guide to Kotlin Coroutines
Threading Made Easy! A Busy Developerā€™s Guide to Kotlin CoroutinesLauren Yew
Ā 
Ef code first
Ef code firstEf code first
Ef code firstZealousysDev
Ā 
AEM & Single Page Applications (SPAs) 101
AEM & Single Page Applications (SPAs) 101AEM & Single Page Applications (SPAs) 101
AEM & Single Page Applications (SPAs) 101Adobe
Ā 
Dependency Injection
Dependency InjectionDependency Injection
Dependency InjectionKnoldus Inc.
Ā 
Spring boot introduction
Spring boot introductionSpring boot introduction
Spring boot introductionRasheed Waraich
Ā 
Spring boot Introduction
Spring boot IntroductionSpring boot Introduction
Spring boot IntroductionJeevesh Pandey
Ā 
Spring Framework - AOP
Spring Framework - AOPSpring Framework - AOP
Spring Framework - AOPDzmitry Naskou
Ā 
Spring Boot
Spring BootSpring Boot
Spring BootJiayun Zhou
Ā 

What's hot (20)

Build RESTful API Using Express JS
Build RESTful API Using Express JSBuild RESTful API Using Express JS
Build RESTful API Using Express JS
Ā 
Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...
Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...
Spring Interview Questions and Answers | Spring Tutorial | Spring Framework T...
Ā 
Jetpack Compose - Androidā€™s modern toolkit for building native UI
Jetpack Compose - Androidā€™s modern toolkit for building native UIJetpack Compose - Androidā€™s modern toolkit for building native UI
Jetpack Compose - Androidā€™s modern toolkit for building native UI
Ā 
Spring Boot Tutorial
Spring Boot TutorialSpring Boot Tutorial
Spring Boot Tutorial
Ā 
Xke spring boot
Xke spring bootXke spring boot
Xke spring boot
Ā 
Spring boot - an introduction
Spring boot - an introductionSpring boot - an introduction
Spring boot - an introduction
Ā 
Spring - Part 1 - IoC, Di and Beans
Spring - Part 1 - IoC, Di and Beans Spring - Part 1 - IoC, Di and Beans
Spring - Part 1 - IoC, Di and Beans
Ā 
Deep dive into swift UI
Deep dive into swift UIDeep dive into swift UI
Deep dive into swift UI
Ā 
REST APIs with Spring
REST APIs with SpringREST APIs with Spring
REST APIs with Spring
Ā 
Swift UI - Declarative Programming [Pramati Technologies]
Swift UI - Declarative Programming [Pramati Technologies]Swift UI - Declarative Programming [Pramati Technologies]
Swift UI - Declarative Programming [Pramati Technologies]
Ā 
Threading Made Easy! A Busy Developerā€™s Guide to Kotlin Coroutines
Threading Made Easy! A Busy Developerā€™s Guide to Kotlin CoroutinesThreading Made Easy! A Busy Developerā€™s Guide to Kotlin Coroutines
Threading Made Easy! A Busy Developerā€™s Guide to Kotlin Coroutines
Ā 
Ef code first
Ef code firstEf code first
Ef code first
Ā 
Spring Boot
Spring BootSpring Boot
Spring Boot
Ā 
AEM & Single Page Applications (SPAs) 101
AEM & Single Page Applications (SPAs) 101AEM & Single Page Applications (SPAs) 101
AEM & Single Page Applications (SPAs) 101
Ā 
Dependency Injection
Dependency InjectionDependency Injection
Dependency Injection
Ā 
Spring Boot
Spring BootSpring Boot
Spring Boot
Ā 
Spring boot introduction
Spring boot introductionSpring boot introduction
Spring boot introduction
Ā 
Spring boot Introduction
Spring boot IntroductionSpring boot Introduction
Spring boot Introduction
Ā 
Spring Framework - AOP
Spring Framework - AOPSpring Framework - AOP
Spring Framework - AOP
Ā 
Spring Boot
Spring BootSpring Boot
Spring Boot
Ā 

Similar to Building Reusable SwiftUI Components

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 2017Elyse 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/4DEVCON
Ā 
Declarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeDeclarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeRamon Ribeiro Rabello
Ā 
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 layoutsDiego 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 routerKaty Slemon
Ā 
WordPress as the Backbone(.js)
WordPress as the Backbone(.js)WordPress as the Backbone(.js)
WordPress as the Backbone(.js)Beau Lebens
Ā 
How to create an Angular builder
How to create an Angular builderHow to create an Angular builder
How to create an Angular builderMaurizio 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ƩutilisablesRiad 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 SidebarRonDosh
Ā 
SciVerse Application Integration Points
SciVerse Application Integration PointsSciVerse Application Integration Points
SciVerse Application Integration PointsElsevier Developers
Ā 
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 GAERon 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 MVCpootsbook
Ā 
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 FirebasePeter Friese
Ā 
Android development for iOS developers
Android development for iOS developersAndroid development for iOS developers
Android development for iOS developersDarryl Bayliss
Ā 
Optimize CollectionView Scrolling
Optimize CollectionView ScrollingOptimize CollectionView Scrolling
Optimize CollectionView ScrollingAndrea 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)

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
Ā 
Declarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeDeclarative UIs with Jetpack Compose
Declarative UIs with Jetpack Compose
Ā 
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
Ā 
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
Ā 
Android query
Android queryAndroid query
Android query
Ā 

More from Peter Friese

Firebase & SwiftUI Workshop
Firebase & SwiftUI WorkshopFirebase & SwiftUI Workshop
Firebase & SwiftUI WorkshopPeter Friese
Ā 
Firebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroesFirebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroesPeter 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 LeedsPeter Friese
Ā 
async/await in Swift
async/await in Swiftasync/await in Swift
async/await in SwiftPeter Friese
Ā 
Firebase for Apple Developers
Firebase for Apple DevelopersFirebase for Apple Developers
Firebase for Apple DevelopersPeter Friese
Ā 
Building Apps with SwiftUI and Firebase
Building Apps with SwiftUI and FirebaseBuilding Apps with SwiftUI and Firebase
Building Apps with SwiftUI and FirebasePeter 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 FirebasePeter 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 AuthPeter 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 AssistantPeter 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 GooglePeter 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.0Peter Friese
Ā 
Google Fit, Android Wear & Xamarin
Google Fit, Android Wear & XamarinGoogle Fit, Android Wear & Xamarin
Google Fit, Android Wear & XamarinPeter Friese
Ā 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android WearPeter Friese
Ā 
Google Play Services Rock
Google Play Services RockGoogle Play Services Rock
Google Play Services RockPeter Friese
Ā 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android WearPeter 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 AndroidPeter 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-InPeter 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 RobolectricPeter 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
Ā 

More from Peter Friese (20)

Firebase & SwiftUI Workshop
Firebase & SwiftUI WorkshopFirebase & SwiftUI Workshop
Firebase & SwiftUI Workshop
Ā 
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
Ā 

Recently uploaded

Intelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalmIntelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalmSujith Sukumaran
Ā 
办ē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·
办ē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·åŠžē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·
办ē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·umasea
Ā 
Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024
Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024
Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024StefanoLambiase
Ā 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureDinusha Kumarasiri
Ā 
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanySuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanyChristoph Pohl
Ā 
Buds n Tech IT Solutions: Top-Notch Web Services in Noida
Buds n Tech IT Solutions: Top-Notch Web Services in NoidaBuds n Tech IT Solutions: Top-Notch Web Services in Noida
Buds n Tech IT Solutions: Top-Notch Web Services in Noidabntitsolutionsrishis
Ā 
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxKnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxTier1 app
Ā 
Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)
Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)
Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)jennyeacort
Ā 
Cloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEECloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEEVICTOR MAESTRE RAMIREZ
Ā 
What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...Technogeeks
Ā 
Unveiling Design Patterns: A Visual Guide with UML Diagrams
Unveiling Design Patterns: A Visual Guide with UML DiagramsUnveiling Design Patterns: A Visual Guide with UML Diagrams
Unveiling Design Patterns: A Visual Guide with UML DiagramsAhmed Mohamed
Ā 
How to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdfHow to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdfLivetecs LLC
Ā 
CRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. SalesforceCRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. SalesforceBrainSell Technologies
Ā 
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed DataAlluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed DataAlluxio, Inc.
Ā 
Unveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New FeaturesUnveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New FeaturesŁukasz Chruściel
Ā 
Cloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStackCloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStackVICTOR MAESTRE RAMIREZ
Ā 
Recruitment Management Software Benefits (Infographic)
Recruitment Management Software Benefits (Infographic)Recruitment Management Software Benefits (Infographic)
Recruitment Management Software Benefits (Infographic)Hr365.us smith
Ā 
ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...
ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...
ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...Christina Lin
Ā 

Recently uploaded (20)

Intelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalmIntelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalm
Ā 
办ē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·
办ē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·åŠžē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·
办ē†å­¦ä½čƁ(UQę–‡å‡­čƁ书)ę˜†å£«å…°å¤§å­¦ęƕäøščÆęˆē»©å•åŽŸē‰ˆäø€ęØ”äø€ę ·
Ā 
Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024
Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024
Dealing with Cultural Dispersion ā€” Stefano Lambiase ā€” ICSE-SEIS 2024
Ā 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with Azure
Ā 
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanySuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
Ā 
Hot Sexy call girls in Patel NagaršŸ” 9953056974 šŸ” escort Service
Hot Sexy call girls in Patel NagaršŸ” 9953056974 šŸ” escort ServiceHot Sexy call girls in Patel NagaršŸ” 9953056974 šŸ” escort Service
Hot Sexy call girls in Patel NagaršŸ” 9953056974 šŸ” escort Service
Ā 
Buds n Tech IT Solutions: Top-Notch Web Services in Noida
Buds n Tech IT Solutions: Top-Notch Web Services in NoidaBuds n Tech IT Solutions: Top-Notch Web Services in Noida
Buds n Tech IT Solutions: Top-Notch Web Services in Noida
Ā 
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxKnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
Ā 
Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)
Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)
Call UsšŸ”>ą¼’+91-9711147426ā‡›Call In girls karol bagh (Delhi)
Ā 
Cloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEECloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEE
Ā 
What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...
Ā 
Unveiling Design Patterns: A Visual Guide with UML Diagrams
Unveiling Design Patterns: A Visual Guide with UML DiagramsUnveiling Design Patterns: A Visual Guide with UML Diagrams
Unveiling Design Patterns: A Visual Guide with UML Diagrams
Ā 
How to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdfHow to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdf
Ā 
CRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. SalesforceCRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. Salesforce
Ā 
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed DataAlluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Ā 
Unveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New FeaturesUnveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New Features
Ā 
2.pdf Ejercicios de programaciĆ³n competitiva
2.pdf Ejercicios de programaciĆ³n competitiva2.pdf Ejercicios de programaciĆ³n competitiva
2.pdf Ejercicios de programaciĆ³n competitiva
Ā 
Cloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStackCloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStack
Ā 
Recruitment Management Software Benefits (Infographic)
Recruitment Management Software Benefits (Infographic)Recruitment Management Software Benefits (Infographic)
Recruitment Management Software Benefits (Infographic)
Ā 
ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...
ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...
ODSC - Batch to Stream workshop - integration of Apache Spark, Cassandra, Pos...
Ā 

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