-
-
Save byJeevan/f67485bf380e86d4f87a723ac383827c to your computer and use it in GitHub Desktop.
SwiftUI Components & Extension that saves iOSDev time. | |
### UIKit is **EVENT-Driven** framework - We could reference each view in the hierarchy, update its appearance when the view is loaded or as a reaction on an event. | |
### SwiftUI is **Declarative, State Driven** framework - We cannot reference any view in the hierarchy, neither can we directly mutate a view as a reaction to an event. | |
Instead, we mutate the state bound to the view. Delegates, target-actions, responder chain, KVO .. replaced with Closures & bindings. |
Color Extension :
/// Hex to Color converter. Usage eg. Color(hex: 0x363CC) or Color(hexString: "#FF0000")
extension Color {
init(hexString: String, alpha: Double = 1.0) {
var cString = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if cString.hasPrefix("#") { cString.removeFirst() }
if cString.count == 6 {
var rgbValue: UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
opacity: alpha)
} else {
self.init(hexString: "#000000")
}
}
init(hex: Int, alpha: Double = 1.0) {
let components = (
R: Double((hex >> 16) & 0xff) / 255,
G: Double((hex >> 08) & 0xff) / 255,
B: Double((hex >> 00) & 0xff) / 255
)
self.init(.sRGB, red: components.R, green: components.G, blue: components.B, opacity: alpha)
}
}
/* CARD SHADOW for any view */
<your view >.background(
RoundedRectangle(cornerRadius: 15)
.foregroundColor(Color.white)
.shadow( color: Color.gray.opacity(0.35),radius: 15, x: 0, y: 0))
/* Full Screen */
<you view>
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
//Navigation bar hidden for a view
NavigationView {
ZStack {
<your content view eleemnts>
}
.navigationBarTitle("Hidden Title")
.navigationBarHidden(self.isNavigationBarHidden)
.onAppear {
self.isNavigationBarHidden = true
}
}
//Share Sheet
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
// nothing to do here
}
}
//Usage :
struct ContentView: View {
@State private var showShareSheet = false
var body: some View {
VStack(spacing: 20) {
Text("Hello World")
Button(action: {
self.showShareSheet = true
}) {
Text("Share Me").bold()
}
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: ["Hello World"])
}
}
}
Width of image = Screen width & aspect ratio retained (height)
Image("workout-image")
.resizable()
.scaledToFit()
let detectDirectionalDrags = DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
.onEnded { value in
print(value.translation)
if value.translation.width < 0 && value.translation.height > -30 && value.translation.height < 30 {
print("left swipe")
}
else if value.translation.width > 0 && value.translation.height > -30 && value.translation.height < 30 {
print("right swipe")
}
else if value.translation.height < 0 && value.translation.width < 100 && value.translation.width > -100 {
print("up swipe")
}
else if value.translation.height > 0 && value.translation.width < 100 && value.translation.width > -100 {
print("down swipe")
}
else {
print("no clue")
}
}
usage .guesture(detectDirectionalDrags)
Corner Radius in specific corner
.background(Rectangle().cornerRadius(40.0, corners: [.topLeft, .topRight]).foregroundColor(Color(0xF4F5FC)))
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
What SwiftUI Transitions Are Available ?
There are 5 transitions available in SwiftUI:
- Slide - slide in the view from the leading side and remove it towards the trailing side.
- Move - slide in from the direction specified and remove it from the same direction.
- Opacity - fade in the view when inserted and fade out the view when removed.
- Scale - insert the view FROM the scale specified and reverse the effect when removed.
- Offset - like the move transition by you can set your own x and y coordinate.
Choices for applying an animation?
- Apply an animation directly to the view.
- Apply an animation to the parent view.
- Use an explicit animation (withAnimation closure) around the variable change that controls when the view is inserted/removed.
⚠️ As of this writing, the opacity and scale only work with explicit animations (withAnimation).
Creating a List with Navigation Link
Note the hierarchy : NavigationView >> List >> NavigationLink
NavigationView {
List(1...10, id: \.self) { index in
if index == 1 {
//do something for row = 1
}
else{
NavigationLink(
destination: Text("Item #\(index) Details"),
label: {
Text("Item #\(index)")
.font(.system(size: 20, weight: .bold, design: .rounded))
})
}
}
.navigationTitle("Latest News")
}
Tabbar/ TabView creation - VStack as parent.
VStack {
TabView {
Text("Video Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "video.circle.fill")
Text("Live")
}
Text("Profile Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
}
.accentColor(.red)
.onAppear() {
UITabBar.appearance().barTintColor = .white
}
}
Tint color and selection/highlight color of tab changed.
Click/Tap/Select/Touch event to view:
@State private var isPresented = false
<your view>.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
if isPresented {
print("Touch down")
}
isPresented = false
}
.onEnded { value in
print("Touch up")
isPresented = true
}
)
.navigationBarHidden(true)
is not constructed for NavigationView
instead, it should be it's child.
Return a view from a function - that checks some conditions.
PS: return types : some View vs AnyView vs View
Ref: https://stackoverflow.com/questions/62895948/how-to-return-a-view-type-from-a-function-in-swift-ui
@ViewBuilder func WidgetDetail(wOption: WidgetOptions) -> some View {
switch wOption.id {
case 0:
CountryPicker()
default:
Text("Widget Not implemented")
}
}
Present view Modally (fullscreen) [ for iOS 14.0+]
@State var isQuizPlayPresented = false
Button(action: {
isQuizPlayPresented.toggle()
}) {
Text("Get Started")
}
.padding()
.fullScreenCover(isPresented: $isQuizPlayPresented, onDismiss: nil, content: QuizPlayView.init)
Dismiss a presented view (modally)
@Environment(\.presentationMode) private var presentationMode // define
self.presentationMode.wrappedValue.dismiss() // In button action
Advanced:
.fullScreenCover(isPresented: $isResultView,
onDismiss: {
self.presentationMode.wrappedValue.dismiss() // Dismisses current view along with Child view dismissed
}, content: {
QuizResultView(totalScore: 10) // Pass custom parameter to child view
})
Make the width of element fill till parent width
VStack(alignment: .center) {
Rectangle()
.frame(height: 0.0) // -------- note this
Text("SHOW DETAILS")
}
.background(.yellow)
Basic Rail :
struct QuizRail: View {
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 16) {
ForEach((1...10), id: \.self) {_ in
Rectangle()
.frame(width: 150, height: 200)
.cornerRadius(8)
}
}
.padding(.horizontal, 16)
}
}
}
List without separator (from iOS 15.0+)
List {
ForEach(items, id: \.self) { item in
Text("item here")
.listRowSeparator(.hidden)
}
}
.listStyle(.plain)
☢️ A workaround calculated the height of ScrollView
@State var heightCalculated = 0.0
var body: some View {
ScrollView {
ForEach(0..<10) { i in
Text("\(i)")
}
.background(
GeometryReader { proxy in
Color.clear.onAppear {
heightCalculated = proxy.size.height
debugPrint(proxy.size.height)
}
}
)
}
.frame(height: heightCalculated)
}
To make Content view as parameter. A better way to build Generic views.
struct ContainerView<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
content
}
}
// Usage
ContainerView{
...
}
Debugging:
let _ = Self._printChanges()
print statement to trace view lifecyclelet _ = print("Update XYZView")
We use this print to trace whether the body of Particular view (eg. SpyView) is executed or not.
ref: https://sarunw.com/posts/how-to-do-print-debugging-in-swiftui/
To make fullscreen view to fill entire screen:
ZStack {
Text("Hello").background(.yellow)
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // this is important
.background(.blue)
// SwiftUI Button