SwiftUI: Understanding .sheet / .fullScreenCover lifecycle when using constant vs #Binding initializers - swift

I'm trying to understand how and when the .sheet and .fullScreenCover initializers are called. Below is a minimal reproducible example, where the first screen has 3 colored rectangles and the SecondView (shown via .fullScreenCover) has 1 rectangle that changes color based on the selected color from the first screen.
When the app first loads, the color is set to .gray.
If I tap on the green rectangle, SecondView presents with a gray rectangle. (ie. the color DIDN'T change correctly).
If I then dismiss the SecondView and tap on the red rectangle, the SecondView presents with a red rectangle. (ie. the color DID change correctly.)
So, I'm wondering why this set up does NOT work on the initial load, but does work on the 2nd/3rd try?
Note: I understand this can be solved by changing the 'let selectedColor' to a #Binding variable, that's not what I'm asking.
Code:
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false
#State var color: Color = .gray
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
color = .red
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
color = .orange
showSheet.toggle()
}
}
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
let selectedColor: Color // Should change to #Binding
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}

See comments and print statements. Especially the red
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false{
didSet{
print("showSheet :: didSet")
}
willSet{
print("showSheet :: willSet")
}
}
#State var color: Color = .gray{
didSet{
print("color :: didSet :: \(color.description)")
}
willSet{
print("color :: willSet :: \(color.description)")
}
}
#State var refresh: Bool = false
init(){
print("SegueTest " + #function)
}
var body: some View {
print(#function)
return HStack {
//Just to see what happens when you recreate the View
//Text(refresh.description)
Text(color.description)
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: red")
//Changing the color
color = .red
//Refreshed SegueTest reloads function
//refresh.toggle()
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: green")
//Changing the color
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: orange")
//Changing the color
color = .orange
showSheet.toggle()
}
}
//This part is likely created when SegueTest is created and since a struct is immutable it keeps the original value
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
//struct is immutable
let selectedColor: Color // Should change to #Binding
init(selectedColor: Color){
print("SecondView " + #function)
self.selectedColor = selectedColor
print("SecondView :: struct :: selectedColor = \(self.selectedColor.description)" )
print("SecondView :: parameter :: selectedColor = \(selectedColor.description)" )
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}

The problem is that you are not using your #State color inside SegueView, which will not reload your view on State change. Just include your #State somewhere in the code, which will force it to rerender at then update the sheet, with the correct color.
RoundedRectangle(cornerRadius: 25)
.fill(color == .red ? Color.red : Color.red) //<< just use dump color variable here
If you do not include your color state somewhere in the code, it won't re render your SegueView, hence you still pass the old color gray to your SecondView.
Or even pass your colors as Binding your your Second View..
SecondView(selectedColor: $color)
#Binding var selectedColor: Color

Related

swiftUI geometryEffect animation

I'm practicing swiftui geometryEffect by applying it to a transition from view to another. The first view has three circles with different colors, the user selects a color by tapping the desired color, and clicks "next" to go to second view which contains a circle with the selected color.
GeometryEffect works fine when transitioning from FirstView to SecondView in which the selected circle's positions animates smoothly, but going back is the problem.
GeometryEffect does not animate the circle position smoothly when going back from SecondView to FirstView. Instead, it seems like it moves the circle to the left and right before positioning the circle to its original position.
Im sharing a GIF on my google drive to show what I mean:
I'd like to achieve something like this: desired result
(file size is too large to be uploaded directly)
Thank you!
FirstView:
struct FirstView: View {
#Namespace var namespace
#State var whichStep: Int = 1
#State var selection: Int = 0
var colors: [Color] = [.red, .green, .blue]
#State var selectedColor = Color.black
var transitionNext: AnyTransition = .asymmetric(
insertion: .move(edge: .trailing),
removal:.move(edge: .leading))
var transitionBack: AnyTransition = .asymmetric(
insertion: .move(edge: .leading),
removal:.move(edge: .trailing))
#State var transition: AnyTransition = .asymmetric(
insertion: .move(edge: .trailing),
removal:.move(edge: .leading))
var body: some View {
VStack {
HStack { // back and next step buttons
Button { // back button
print("go back")
withAnimation(.spring()) {
whichStep = 1
transition = transitionBack
}
} label: {
Image(systemName: "arrow.backward")
.font(.system(size: 20))
}
.padding()
Spacer()
Button { // next button
withAnimation(.spring()) {
whichStep = 2
transition = transitionNext
}
} label: {
Text("next step")
}
}
Spacer()
if whichStep == 1 {
ScrollView {
ForEach(0..<colors.count, id:\.self) { color in
withAnimation(.none) {
Circle()
.fill(colors[color])
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: color, in: namespace)
.onTapGesture {
selectedColor = colors[color]
selection = color
}
}
}
}
Spacer()
} else if whichStep == 2 {
// withAnimation(.spring()) {
ThirdView(showScreen: $whichStep, namespace: namespace, selection: $selection, color: selectedColor)
.transition(transition)
// }
Spacer()
}
}
.padding()
}
}
SecondView:
struct ThirdView: View {
var namespace: Namespace.ID
#Binding var selection: Int
#State var color: Color
var body: some View {
VStack {
GeometryReader { geo in
Circle()
.fill(color)
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: selection, in: namespace)
}
}
.ignoresSafeArea()
}
}

How to to add Blur overlay on top of all views using .Blur as ViewModifier in SwiftUI

I have created a viewModifier which blurs a view when added.
Problem is when I add it on parent view of all the contents, all views are blurred differently.
I assume it's because it goes to all the contents and blur each of them individually instead of adding an overlay blur to the parent view.
I used Swift UI's native blur because UIBlurEffect has very limited configuration i.e. I can't adjust the blur intensity to a specific value.
Here it is when the modifier is added in the parent ZStack:
Here when I add it on background image only:
Adding it to background image looks good but I need it to be on top of all views. Here is my code:
Main View
import SwiftUI
struct BlurViewDemo: View {
#State private var isPressed = true
#State private var blurColor: BlurColor = .none
var body: some View {
ZStack {
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
Image("blur-view-demo-image")
.resizable()
.frame(width: frame.size.width, height: frame.size.height)
// When added here only the background image is blurred.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
VStack(spacing: 30) {
// TITLE STYLE
Text(getSelectedBlurTitle())
.font(.system(size: 60))
.offset(y: -40)
.foregroundColor(
blurColor == .dark && isPressed ? .white : .black
)
// TOGGLE BLUR BUTTON
Button(action: {
isPressed.toggle()
}, label: {
Text(isPressed ? "Blur: On" : "Blur: Off")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color(#colorLiteral(red: 0.2, green: 0.4, blue: 0.4, alpha: 1)))
// DEFAULT BUTTON
Button(action: {
isPressed = true
blurColor = .none
}, label: {
Text("Default")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.gray)
// LIGHT BUTTON
Button(action: {
isPressed = true
blurColor = .light
}, label: {
Text("Light")
.foregroundColor(.black)
})
.frame(width: 80, height: 40)
.background(Color.white)
// DARK BUTTON
Button(action: {
isPressed = true
blurColor = .dark
}, label: {
Text("Dark")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.black)
} //: VSTACK
} //: ZSTACK
.edgesIgnoringSafeArea(.all)
// When added here, the buttons are not blurred properly.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
private func getSelectedBlurTitle() -> String {
guard isPressed else { return "Clear"}
switch blurColor {
case .none:
return "Default"
case .light:
return "Light"
case .dark:
return "Dark"
}
}
}
struct BlurViewDemo_Previews: PreviewProvider {
static var previews: some View {
BlurViewDemo()
}
}
View Modifier
public enum BlurColor {
case none
case light
case dark
}
import SwiftUI
struct BlurModifier: ViewModifier {
#Binding private var showBlur: Bool
#Binding private var blurColor: BlurColor
#State private var blurRadius: CGFloat = 14
public init(showBlur: Binding<Bool>, blurColor: Binding<BlurColor>) {
self._showBlur = showBlur
self._blurColor = blurColor
}
func body(content: Content) -> some View {
ZStack {
content
.blur(radius: showBlur ? blurRadius : 0, opaque: true)
.animation(Animation.easeInOut(duration: 0.3))
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(showBlur ? true : false)
Rectangle()
.fill(showBlur ? getBlurColor() : Color.white.opacity(0.0001))
.edgesIgnoringSafeArea(.all)
}
}
private func getBlurColor() -> Color {
switch blurColor {
case .none:
return Color.white.opacity(0.5)
case .light:
return Color.white.opacity(0.6)
case .dark:
return Color.black.opacity(0.5)
}
}
}

How do I make conjoined buttons in SwiftUI? (MacOS)

There are buttons present in apps, such as TextEdit where the sides of adjacent buttons are joined together. Does anyone know how I could replicate this visual? I'm working in SwiftUI on MacOS, and answers for MacOS 10 & 11 would be appreciated.
I don’t think that’s possible in SwiftUI, unless you want to build it from scratch. What you’re seeing in TextEdit is an NSSegmentedControl that allows multiple selection: https://developer.apple.com/design/human-interface-guidelines/macos/selectors/segmented-controls/
In SwiftUI, segmented controls are made using Picker, which doesn’t allow multiple selection. Your best bet is to wrap NSSegmentedControl in an NSHostingView.
Replicating these buttons using SwiftUI is not all that difficult.
Here is a very basic approach, adjust the colors, corners etc... to your liking:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
TextFormats()
}
}
struct GrayButtonStyle: ButtonStyle {
let w: CGFloat
let h: CGFloat
init(w: CGFloat, h: CGFloat) {
self.w = w
self.h = h
}
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(Color.white)
.frame(width: w, height: h)
.padding(5)
.background(Color(UIColor.systemGray4))
.overlay(Rectangle().strokeBorder(Color.gray, lineWidth: 1))
}
}
struct TextFormats: View {
let sx = CGFloat(20)
let color = Color.black
#State var bold = false
#State var italic = false
#State var underline = false
var body: some View {
HStack (spacing: 0) {
Group {
Button(action: { bold.toggle() }) {
Image(systemName: "bold").resizable().frame(width: sx, height: sx)
.foregroundColor(bold ? .blue : color)
}
Button(action: { italic.toggle() }) {
Image(systemName: "italic").resizable().frame(width: sx, height: sx)
.foregroundColor(italic ? .blue : color)
}
Button(action: { underline.toggle() }) {
Image(systemName: "underline").resizable().frame(width: sx, height: sx)
.foregroundColor(underline ? .blue : color)
}
}.buttonStyle(GrayButtonStyle(w: sx+5, h: sx+5))
}.padding(1)
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(.white, lineWidth: 2))
.clipped(antialiased: true)
}
}

Align views in Picker

How do I align the Color views in a straight line with the text to the side?
To look like so (text aligned leading):
█  red
█  green
█  blue
Or this (text aligned center):
█    red
█  green
█   blue
Current code:
struct ContentView: View {
#State private var colorName: Colors = .red
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
}
}
}
}
}
enum Colors: String, CaseIterable, Identifiable {
case red
case green
case blue
var id: String { rawValue }
var asColor: Color {
switch self {
case .red: return .red
case .green: return .green
case .blue: return .blue
}
}
}
Result (not aligned properly):
Without the Picker, I found it is possible to use alignmentGuide(_:computeValue:) to achieve the result. However, this needs to be in a Picker.
Attempt:
VStack(alignment: .custom) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
.alignmentGuide(.custom) { d in
d[.leading]
}
Text(color.rawValue)
.frame(maxWidth: .infinity)
.fixedSize()
}
}
.frame(height: 50)
}
/* ... */
extension HorizontalAlignment {
struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[HorizontalAlignment.leading]
}
}
static let custom = HorizontalAlignment(CustomAlignment.self)
}
Result of attempt:
Possible solution is to use dynamic width for labels applied by max calculated one using view preferences.
Here is a demo. Tested with Xcode 13beta / iOS15
Note: the ViewWidthKey is taken from my other answer https://stackoverflow.com/a/63253241/12299030
struct ContentView: View {
#State private var colorName: Colors = .red
#State private var maxWidth = CGFloat.zero
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
}
.background(GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.frame(in: .local).size.width)
})
.onPreferenceChange(ViewWidthKey.self) {
self.maxWidth = max($0, maxWidth)
}
.frame(minWidth: maxWidth, alignment: .leading)
}
}
}
}
I think this should align your text and fix your issue.
struct ContentView: View {
#State private var colorName: Colors = .red
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack() {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
.frame(width: 100, height: 30, alignment: .leading)
}
}
}
}
}
A simple .frame modifier will fix these issues, try to frame your text together or use a Label if you don't want to complicate things when it comes to pickers or list views.
If you do go forward with this solution try to experiment with the width and height based on your requirements, and see if you want .leading, or .trailing in the alignment

SwiftUI animated view weird transition when appears inside ZStack

I have a problem regarding behavior of an animated loading view. The loading view shows up while the network call. I have an isLoading #Published var inside my viewModel and the ActivityIndicator is shown inside a ZStack in my view. The Activityindicator is a custom view where I animate a trimmed circle - rotate it. Whenever the activity indicator is shown inside my mainView it has a weird transition when appearing- it is transitioned from the top left corner to the center of the view. Does anyone know why is this happening? I attach the structs with the code and 3 pictures with the behavior.
ActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
CircleView(animate: $animate, firstGradientColor: orangeColor, secondGradientColor: orangeColorOpaque, style: style)
}.onAppear() {
self.animate.toggle()
}
}
}
struct CircleView: View {
#Binding var animate: Bool
var firstGradientColor: Color
var secondGradientColor: Color
var style: StrokeStyle
var body: some View {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [firstGradientColor, secondGradientColor]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.transition(.opacity)
.animation(Animation.linear(duration: 0.7) .repeatForever(autoreverses: false), value: animate)
}
}
The view is use it in :
struct UserProfileView: View {
#ObservedObject var viewModel: UserProfileViewModel
#Binding var lightMode: ColorScheme
var body: some View {
NavigationView {
ZStack {
VStack(alignment: .center, spacing: 12) {
HStack {
Text(userProfileEmail)
.font(.headline)
.foregroundColor(Color(UIColor.label))
Spacer()
}.padding(.bottom, 16)
SettingsView(userProfile: $viewModel.userProfile, isDarkMode: $viewModel.isDarkMode, lightMode: $lightMode, location: viewModel.locationManager.address, viewModel: viewModel)
ButtonsView( userProfile: $viewModel.userProfile)
Spacer()
}.padding([.leading, .trailing], 12)
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
// .animation(nil)
}
}
}
}
}
I also tried with animation nil but it doesn't seem to work.
Here are the pictures:
Here is a possible solution - put it over NavigationView. Tested with Xcode 12.4 / iOS 14.4
NavigationView {
// .. your content here
}
.overlay( // << here !!
VStack {
if isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
)