SwiftUI : Why preferenceKey is not read/called - ios

From the code below, I can't figure out why Preference key is not read from the view put in background of HStack. Strangely, If I put that BGViewSetter to the Text Views inside ForEach loop, it works. Unless I have ommitted something too obvious, it looks like a bug. Anybody can confirm it? Thanks
xcode12.2
ios 14.2
struct TestRectPreference : Equatable
{
var frame : CGRect
}
struct TestRectPreferenceKey : PreferenceKey
{
typealias Value = TestRectPreference
static var defaultValue: TestRectPreference = TestRectPreference(frame: CGRect.zero)
static func reduce(value: inout TestRectPreference, nextValue: () -> TestRectPreference)
{
value.frame = nextValue().frame
}
}
struct BGViewSetter: View {
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 25.0)
.fill(Color.init(#colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1)))
.preference(key: TestRectPreferenceKey.self,
value: TestRectPreference(frame: geometry.frame(in: .global)))
}
}
}
struct FinalView : View
{
#State var offsetX : CGFloat = .zero
#State var info = ""
#State var size : CGFloat = .zero
var body: some View
{
VStack
{
Text("FinalView : \(info)")
HStack
{
ForEach( 1 ..< 10)
{ i in
Text("\(i)")
.frame(width: 100)
.opacity(0.8)
}
}
.background(BGViewSetter())
.animation(.easeOut)
.gesture(
DragGesture()
.onChanged
{ gesture in
self.offsetX = gesture.translation.width
}
.onEnded
{ _ in
self.offsetX = .zero
}
)
.offset(x: self.offsetX)
Text("Footer")
Divider()
Spacer()
}
.onPreferenceChange(TestRectPreferenceKey.self)
{
self.size = $0.frame.height
self.info = "Pref Chng : \(self.size)"
}
}
}
struct PreferenceKeyTest_Previews: PreviewProvider {
static var previews: some View {
FinalView()
}
}

Related

How can I add a more rows of buttons in my frame when my hstack is too long in Swift UI?

I would like to make the second row appear when my list is too long.
Do you have any idea how to do that?
Thank you in advance!
import SwiftUI
struct ContentView: View {
#StateObject var vm = SpeakingVM()
var speakingModel: SpeakingModel
var body: some View {
HStack(spacing:20){
ForEach(speakingModel.sentence.indices) { index in
Button(action: {
}, label: {
Text(speakingModel.sentence[index].definition)
.padding(.vertical,10)
.padding(.horizontal)
.background(Capsule().stroke(Color.blue))
.lineLimit(1)
})
}
}.frame(width: UIScreen.main.bounds.width - 30, height: UIScreen.main.bounds.height / 3)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(speakingModel: SpeakingModel(sentence: [SpeakingModel.Word(definition: "Météo"),SpeakingModel.Word(definition: "Cheval"),SpeakingModel.Word(definition: "Ascenceur")], sentenceInFrench: "Quel temps fait-il ?"))
}
}
What i would like :
Put your data in tags and customize the item view and change it appropriately.
import SwiftUI
struct HashTagView: View {
#State var tags: [String] = ["#Lorem", "#Ipsum", "#dolor", "#consectetur", "#adipiscing", "#elit", "#Nam", "#semper", "#sit", "#amet", "#ut", "#eleifend", "#Cras"]
#State private var totalHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.tags, id: \.self) { tag in
self.item(for: tag)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if tag == self.tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == self.tags.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
struct HashTagView_Previews: PreviewProvider {
static var previews: some View {
HashTagView()
}
}

SwiftUI: Reset "Slide to Unlock" button after drag gesture

I want to make my slider go back to its original position after I have unlocked it once by dragging to the end.
The animation goes till the end and stays there.
I want it to go back to its initial position once the unlock swipe is complete, meaning once the unlock lock image is shown, it should reset to again being locked.
How can I achieve this?
I am attaching complete code. It has three components, Dragging component and background component that are being called in main Unlock button view.
//
// DraggingComponent.swift
// VirtualDoorman
//
// Created by Engr. Bushra on 11/9/22.
//
import SwiftUI
struct DraggingComponent: View {
#Binding var isLocked: Bool
let isLoading: Bool
let maxWidth: CGFloat
private let minWidth = CGFloat(50)
#State private var width = CGFloat(50)
var duration: Double = 0.3
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(Color.red)
.opacity(width / maxWidth)
.frame(width: width)
.overlay(
Button(action: { }) {
ZStack {
image(name: "lock", isShown: isLocked)
progressView(isShown: isLoading)
image(name: "lock.open", isShown: !isLocked && !isLoading)
}
.animation(.easeIn(duration: 0.35).delay(0.55), value: !isLocked && !isLoading)
}
.buttonStyle(BaseButtonStyle())
.disabled(!isLocked || isLoading),
alignment: .trailing
)
.simultaneousGesture (
DragGesture()
.onChanged { value in
guard isLocked else { return }
if value.translation.width > 0 {
width = min(max(value.translation.width + minWidth, minWidth), maxWidth)
}
}
.onEnded { value in
guard isLocked else { return }
if width < maxWidth {
width = minWidth
UINotificationFeedbackGenerator().notificationOccurred(.warning)
} else {
UINotificationFeedbackGenerator().notificationOccurred(.success)
withAnimation(.spring().delay(0.5)) {
isLocked = false
// DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.2) {
// withAnimation(.easeOut(duration: duration)) {
// width = min(max(value.translation.width + minWidth, minWidth), maxWidth)
//
// }
// }
// isLocked = true
}
}
}
)
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 0), value: width)
}
private func image(name: String, isShown: Bool) -> some View {
Image(systemName: name)
.font(.system(size: 20, weight: .regular, design: .rounded))
.foregroundColor(Color("BlueAccent"))
.frame(width: 42, height: 42)
.background(RoundedRectangle(cornerRadius: 14).fill(.white))
.padding(4)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
private func progressView(isShown: Bool) -> some View {
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
}//struct
//struct DraggingComponent_Previews: PreviewProvider {
// static var previews: some View {
// DraggingComponent(maxWidth: 10)
// }
//}
struct BaseButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.opacity(configuration.isPressed ? 0.9 : 1)
.animation(.default, value: configuration.isPressed)
}
}
//
// BackgroundComponent.swift
// VirtualDoorman
//
// Created by Engr. Bushra on 11/9/22.
//
import SwiftUI
struct BackgroundComponent: View {
#State private var hueRotation = false
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 16)
.fill(LinearGradient(
colors: [Color("BlueAccent").opacity(0.8), Color("BlueAccent").opacity(0.8)],
startPoint: .leading,
endPoint: .trailing
)
)
.hueRotation(.degrees(hueRotation ? 20 : -20))
Text("Slide to unlock")
.font(.footnote)
.bold()
.foregroundColor(.white)
.frame(maxWidth: .infinity)
}
.onAppear {
withAnimation(.linear(duration: 3).repeatForever(autoreverses: true)) {
hueRotation.toggle()
}
}
}
}
struct BackgroundComponent_Previews: PreviewProvider {
static var previews: some View {
BackgroundComponent()
}
}
extension Color {
static let pinkBright = Color(red: 247/255, green: 37/255, blue: 133/255)
static let blueBright = Color(red: 67/255, green: 97/255, blue: 238/255)
static let blueDark = Color(red: 58/255, green: 12/255, blue: 163/255)
}
//
// Unlock_Button.swift
// VirtualDoorman
//
// Created by Engr. Bushra on 11/9/22.
//
import SwiftUI
struct Unlock_Button: View {
#State private var isLocked = true
#State private var isLoading = false
var completion: (() -> Void)?
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
BackgroundComponent()
DraggingComponent(isLocked: $isLocked, isLoading: isLoading, maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
}
}
private func simulateRequest() {
isLoading = true
// completion?()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
isLoading = false
}
}
}
struct Unlock_Button_Previews: PreviewProvider {
static var previews: some View {
Unlock_Button()
}
}

how to infinite rotation in circular way by SwiftUI?

I could not rotate the green part in the circular path.
I want to set dynamically infinite time.
I created a struct for ProgressView and want to set it as ProgressView in my project.
how to implement the red and black colour curve design in the rotation
path
struct CircularProgressView: View {
#State var progressValue: Float = 0.0
var body: some View {
ZStack {
ProgressBar(progress: self.$progressValue)
.frame(width: 150.0, height: 150.0)
.padding(40.0)
}.onAppear{
self.incrementProgress()
}
}
func incrementProgress() {
let randomValue = Float([0.012, 0.022, 0.034, 0.016, 0.11, 0.012].randomElement()!)
self.progressValue += randomValue
}
}
struct ProgressBar: View {
#Binding var progress: Float
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.red)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 2.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.green)
.rotationEffect(Angle(degrees: 270))
.animation(.linear)
}
}
}
This is the Preview struct.
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
CircularProgressView()
}
}
Thanks in advance for helping me.
struct CircularProgressView: View {
#State var progressValue: Float = 0.0
#State private var isLoaderVisible: Bool = false
var degree = 90
var body: some View {
ZStack {
ProgressBar(progress: self.$progressValue)
.frame(width: 150.0, height: 150.0)
.padding(40.0)
}.overlay(LoaderView(showLoader: isLoaderVisible))
.onAppear {
isLoaderVisible.toggle()
}
}
}
struct ProgressBar: View {
#Binding var progress: Float
#State var degree:Double = 90
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.foregroundColor(Color.green)
}
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
CircularProgressView()
}
}
// enum for LoaderView
public enum LoaderAnimation {
case low, medium, high
var animationSpeed: Double {
switch self {
case .low: return 1.0
case .medium: return 0.8
case .high: return 10.2
}
}
}
This this the LoaderView which we set on overlay and animate
public struct LoaderView: View {
var loaderAnimationSpeed: LoaderAnimation? = .high
var showLoader: Bool = false
public var body: some View {
GeometryReader { reader in
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.yellow)
Circle()
.trim(from: 0.6, to: 1)
.stroke(.green, lineWidth: 20)
.rotationEffect(.degrees(showLoader ? 360 : 0))
.animation(Animation.easeInOut(duration: showLoader ? loaderAnimationSpeed?.animationSpeed ?? 0.0 : 1).repeatForever(autoreverses: false), value: showLoader)
}
}
}
public init( loaderAnimationSpeed: LoaderAnimation? = .medium, showLoader: Bool) {
self.loaderAnimationSpeed = loaderAnimationSpeed
self.showLoader = showLoader
}
}

SwiftUI. Subview animation is not working if subview's View #State was changed while parent View is animating. iOS 16

I have a structure, where the parent view is a hidden pop-up with content. On some user action parent View starts to slide up. I need all its content to slide up with the parent View. It was working perfectly before iOS 16, but now it's broken. If the child's View subview #State is changed during the animation, then this View appears instantly on a screen, i.e. not sliding. As I understand, because View's #State was changed SwiftUI redraws this particular View and disables its animation, so it appears in its final position without animation. I was trying to force the animation of this particular View using .animation or withAnimation. Nothing of it helped. How to fix this bug in iOS 16?
Minimal reproducible example:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State var shouldShow: SlideCardPosition = .bottom
#State var text1: String = ""
var body: some View {
VStack {
Button {
shouldShow = .top
} label: {
Text("Show PopUp")
.padding(.top, 100)
}
PopUpUIView(shouldShow: $shouldShow)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct SlideOverCard<Content: View>: View {
#GestureState private var dragState = SlideDragState.inactive
#Binding private var position: SlideCardPosition
#State private var highlightedBackground = false
private var contentHeight: CGFloat
private var backgroundColor: Color?
private var withCorners: Bool
private var isHandleHidden: Bool
private var overlayOpacity: CGFloat
init(position: Binding<SlideCardPosition>,
contentHeight: CGFloat,
backgroundColor: Color? = nil,
withCorners: Bool = true,
isHandleHidden: Bool = false,
overlayOpacity: CGFloat = 0.75,
content: #escaping () -> Content) {
_position = position
self.content = content
self.contentHeight = contentHeight
self.backgroundColor = backgroundColor
self.withCorners = withCorners
self.isHandleHidden = isHandleHidden
self.overlayOpacity = overlayOpacity
}
var content: () -> Content
var body: some View {
return Rectangle()
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.foregroundColor(Color.black.opacity(highlightedBackground ? overlayOpacity : 0))
.position(x: UIScreen.screenWidth / 2, y: (UIScreen.screenHeight) / 2)
.edgesIgnoringSafeArea([.top, .bottom])
.overlay(
Group {
VStack(spacing: 0) {
if !isHandleHidden {
Handle()
}
self.content()
Spacer()
}
}
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.background(backgroundColor != nil ? backgroundColor! : Color.black)
.cornerRadius(withCorners ? 40.0 : 0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: position(from: position) + dragState.translation.height)
.animation(dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0), value: UUID())
.edgesIgnoringSafeArea([.top, .bottom])
.onTapGesture {}
.onChange(of: position) { _ in
withAnimation(.easeInOut) {
highlightedBackground.toggle()
}
}
)
.onTapGesture {
position = position == .bottom ? .top : .bottom
}
}
private func position(from cardPosition: SlideCardPosition) -> CGFloat {
switch cardPosition {
case .top: return UIScreen.screenHeight - contentHeight - UIScreen.topSafeAreaHeight
case .bottom: return 1000
}
}
}
enum SlideCardPosition {
case top
case bottom
}
private enum SlideDragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
private struct Handle: View {
private let handleThickness: CGFloat = 5
var body: some View {
RoundedRectangle(cornerRadius: handleThickness / 2.0)
.frame(width: 34, height: handleThickness)
.foregroundColor(.white)
.padding(.top, 8)
}
}
import UIKit
extension UIScreen {
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
private static let window = UIApplication.shared.windows[0]
private static let safeFrame = window.safeAreaLayoutGuide.layoutFrame
static var topSafeAreaHeight: CGFloat {
safeFrame.minY
}
static var bottomSafeAreaHeight: CGFloat {
window.frame.maxY - safeFrame.maxY
}
}
import SwiftUI
struct PopUpUIView: View {
#Binding var shouldShow: SlideCardPosition
#State var text1 = "some random text"
var body: some View {
SlideOverCard(position: $shouldShow,
contentHeight: 300) {
VStack(spacing: 10) {
Text(text1)
.foregroundColor(.white)
.padding(.top, 80)
}
}.onChange(of: shouldShow) { _ in
if shouldShow == .top {
text1 = UUID().uuidString
}
}
}
}
struct PopUpUIView_Previews: PreviewProvider {
static var previews: some View {
PopUpUIView(shouldShow: .constant(.bottom))
}
}
Example of incorrect animation with a dynamic text.
Example of what I want to achieve. It is working fine if text is static.

How to pass and access property from one view to another in SwiftUI?

I have one view which I am using in another view, and i want to access property from one view to another...but i am not sure how to do it.. (My main problem with below code - I really appreciate if I got solution for this - is I am having dropdown even when it is collapsed its expanding when i clicked outside of it..which is wrong)
import SwiftUI
public struct TopSheet<Content>: View where Content : View {
private var content: () -> Content
let minHeight: CGFloat = 50.0
let startHeight: CGFloat = 50.0
let maxOpacity: CGFloat = 0.8
let maxArrowOffset: CGFloat = 8.0
let minimumContentHeight = 61.0
#State private var currentHeight: CGFloat = 0
#State private var contentHeight: CGFloat = 0
#State private var backgroundColor: Color = Color.clear
#State private var expand = false
#State private var arrowOffset: Double = 0
public init(#ViewBuilder content: #escaping () -> Content) { self.content = content }
public func expandRatio() -> Double { return max((currentHeight - minHeight) / contentHeight, 0) }
private func isTopSheetExpandable() -> Bool {
return contentHeight > minimumContentHeight
}
public var body: some View {
let tap = TapGesture()
.onEnded { _ in
expand.toggle()
if expand {
withAnimation(Animation.easeOut) {
currentHeight = max(contentHeight, minHeight)
self.backgroundColor = Color.grey
}
}
else {
withAnimation(Animation.easeOut) {
currentHeight = minHeight
self.backgroundColor = Color.clear
}
}
self.arrowOffset = expandRatio() * maxArrowOffset
}
let drag = DragGesture()
.onChanged { value in
currentHeight += value.translation.height
currentHeight = max(currentHeight, minHeight)
let opacity = min(expandRatio() * maxOpacity, maxOpacity)
self.backgroundColor = Color.gray.opacity(opacity)
self.arrowOffset = expandRatio() * maxArrowOffset
}
.onEnded { value in
expand.toggle()
if expand {
withAnimation(Animation.easeOut) {
currentHeight = max(contentHeight, minHeight)
self.backgroundColor = Color.gray.opacity(maxOpacity)
}
}
else {
withAnimation(Animation.easeOut) {
currentHeight = minHeight
self.backgroundColor = Color.clear
}
}
self.arrowOffset = expandRatio() * maxArrowOffset
}
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
VStack(spacing: 0) {
GeometryReader { geo in
content()
.viewHeight()
.fixedSize(horizontal: false, vertical: true)
}.onPreferenceChange(ViewHeightPreferenceKey.self) { height in
contentHeight = height
currentHeight = startHeight
}.clipped()
Spacer(minLength: 0)
}
.frame(height: currentHeight)
HStack(alignment: .center) {
Spacer()
if isTopSheetExpandable() {
Arrow(offset: arrowOffset)
.stroke(Color.gray, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.frame(width: 30, height: 4)
.padding(.bottom, 10)
.padding(.top, 10)
}
Spacer()
}
// .contentShape(Rectangle())
.gesture(drag)
.gesture(tap)
.animation(.easeInOut, value: 2)
}
.background(Color.white)
Spacer()
}
.background(self.backgroundColor.edgesIgnoringSafeArea([.vertical, .horizontal, .leading, .trailing, .top, .bottom]))
.simultaneousGesture(isTopSheetExpandable() ? tap : nil)
}
}
fileprivate struct Arrow: Shape {
var offset: Double
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: .zero)
path.addLine(to: CGPoint(x: rect.width/2, y: -offset))
path.move(to: CGPoint(x: rect.width/2, y: -offset))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
Another view
import SwiftUI
struct NextView: View {
#State private var backgroundColor: Color = Color.gray
#State private var users: [String] = ["abc", "xyz", "pqr", "mno", "pqr", "ert", ""]
var accessibilityID: String
var body: some View {
ZStack {
Rectangle()
.fill()
.foregroundColor(backgroundColor)
TopSheet {
VStack(spacing: 0) {
ForEach($users, id: \.self) { user in
HStack {
Text(user.wrappedValue)
.padding(.vertical, 10)
Spacer()
}
.contentShape(Rectangle())
.simultaneousGesture(TapGesture().onEnded {
self.users = [user.wrappedValue] + self.users.filter { $0 != user.wrappedValue }
switch self.users.first.unsafelyUnwrapped {
case "Joe Black": self.backgroundColor = .black
case "Eva Green": self.backgroundColor = .green
case "Jared Leto": self.backgroundColor = .red
default: self.backgroundColor = .gray
}
})
}
}
.padding(.horizontal, 10)
}
}
}
}
I might try some trick if I can able to access "expand" property from "TopSheet" and access it in "NextView"
You should store expand as #State in your parent View and pass it via Binding. Simplified example:
public struct TopSheet<Content>: View where Content : View {
#Binding var expand : Bool
var content: () -> Content
//declare other properties private so they don't get used in the generated init
public var body: some View {
Text("Here")
}
}
struct NextView: View {
#State private var expand = false
var body: some View {
TopSheet(expand: $expand) {
Text("Content")
}
}
}
If you really want/need your explicit init, you can do this:
public struct TopSheet<Content>: View where Content : View {
#Binding private var expand : Bool
private var content: () -> Content
public init(expand: Binding<Bool>, #ViewBuilder content: #escaping () -> Content) {
self._expand = expand
self.content = content
}
public var body: some View {
Text("Here")
}
}

Resources