How to use UserDefault on an Array in SwiftUI - swiftui

I'm working on a checklist app that has several arrays with checks. I'd like to save the state if a users closes/quits the app. I was thinking of using the UserDefault methods for this:
HStack {
ForEach(0 ..< checklist.steps) { index in
VStack {
Button(action: {
self.checked[index].toggle()
UserDefaults.standard.set(self.checked[index], forKey: "Check")
I'm currently using the following state for checks:
#State private var checked = [false, false, false, false, false, false]
Does anyone know how to apply UserDefaults for arrays or generally how to save the state for your app when closing it?
Thanks in advance!

try this: (it is the code from last time ;)) i think the true/false settings itself is not correct, but the saving / loading works ;)
struct ChecklistView: View {
var checklist: Checklist
#State var currentProgress: Float = 0.0
#State var checked : [Bool] = [false, false, false, false]
init(checklist: Checklist) {
self.checklist = checklist
}
func saveUserDefaults() {
var value = ""
for eachValue in checked {
if eachValue == true { value += "1" }
else { value += "0" }
}
UserDefaults.standard.set(value, forKey: checklist.title)
}
var body: some View {
ZStack(alignment: .leading) {
// RoundedRectangle(cornerRadius: 20)
// .foregroundColor(.red).opacity(0.5)
// .frame(width: 200)
VStack {
HStack(alignment: .top) {
checklist.image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.padding(.trailing)
VStack(alignment: .leading) {
Text(checklist.title)
.font(.system(size: 16, weight: .medium))
.padding(.bottom, 4)
Text(checklist.instruction.uppercased()).font(.system(size: 12))
HStack {
ForEach(0 ..< checklist.steps) { index in
VStack {
Button(action: {
self.checked[index].toggle()
print(self.checked)
self.saveUserDefaults()
}) {
ZStack {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(self.checked[index] ? Color("LightGreen") : .gray )
.frame(width: 40, height: 40)
Image(systemName: self.checked[index] ? "checkmark" : "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(self.checked[index] ? .white : .white)
}
}
}
}
}
}
}.frame(width: 350, alignment: .leading)
}
}
.onAppear() {
if let values = UserDefaults.standard.value(forKey: self.checklist.title) as? String {
self.checked = values.map {
if $0 == "1" { return true }
return false
}
}
print(self.checked)
}
.frame(width: 350)
.padding(.bottom, 16)
.cornerRadius(8)
.padding(.top, 20)
}
}

I haven't test that solution, but can't you store your array in your NSUserdefaults after every button action?
Button(action: {
self.checked[index].toggle()
UserDefaults.standard.set(self.checked, forKey: "Check")
Probably, the best solution would be using CoreData for that, which would be very easier to use.
PS: I can test that solution later on.. not at my Mac right now

Related

resizable header in SwiftUI

I was trying to make resizable header but it breaks.
I am trying to clone twitter profile,
I think logic is right but can I know why this one is not working?
I made HStack and try to hide it but when I scroll back it can't come back.
Tried with GeometryReader
Please help me, thanks
import SwiftUI
struct ProfileView: View {
// change views
#State var isHide = false
#State private var selectionFilter: TweetFilterViewModel = .tweets
#Namespace var animation
var body: some View {
VStack(alignment: .leading) {
//hiding
if isHide == false {
headerView
actionButtons
userInfoDetails
}
tweetFilterBar
.padding(0)
ScrollView(showsIndicators: false) {
LazyVStack {
GeometryReader { reader -> AnyView in
let yAxis = reader.frame(in: .global).minY
let height = UIScreen.main.bounds.height / 2
if yAxis < height && !isHide {
DispatchQueue.main.async {
withAnimation {
isHide = true
}
}
}
if yAxis > 0 && isHide {
DispatchQueue.main.async {
withAnimation {
isHide = false
}
}
}
return AnyView(
Text("")
.frame(width: 0, height: 0)
)
}
.frame(width: 0, height: 0)
ForEach(0...9, id: \.self) { _ in
TweetRowView()
}
}
}
Spacer()
}
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView()
}
}
extension ProfileView {
var headerView: some View {
ZStack(alignment: .bottomLeading) {
Color(.systemBlue)
.ignoresSafeArea()
VStack {
Button {
} label: {
Image(systemName: "arrow.left")
.resizable()
.frame(width: 20, height: 16)
.foregroundColor(.white)
.position(x: 30, y: 12)
}
}
Circle()
.frame(width: 72, height: 72)
.offset(x: 16, y: 24)
}.frame(height: 96)
}
var actionButtons: some View {
HStack(spacing: 12){
Spacer()
Image(systemName: "bell.badge")
.font(.title3)
.padding(6)
.overlay(Circle().stroke(Color.gray, lineWidth: 0.75))
Button {
} label: {
Text("Edit Profile")
.font(.subheadline).bold()
.accentColor(.black)
.padding(10)
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.gray, lineWidth: 0.75))
}
}
.padding(.trailing)
}
var userInfoDetails: some View {
VStack(alignment: .leading) {
HStack(spacing: 4) {
Text("Heath Legdet")
.font(.title2).bold()
Image(systemName: "checkmark.seal.fill")
.foregroundColor(Color(.systemBlue))
}
.padding(.bottom, 2)
Text("#joker")
.font(.subheadline)
.foregroundColor(.gray)
Text("Your mom`s favorite villain")
.font(.subheadline)
.padding(.vertical)
HStack(spacing: 24) {
Label("Gothem.NY", systemImage: "mappin.and.ellipse")
Label("www.thejoker.com", systemImage: "link")
}
.font(.caption)
.foregroundColor(.gray)
HStack(spacing: 24) {
HStack {
Text("807")
.font(.subheadline)
.bold()
Text("following")
.font(.caption)
.foregroundColor(.gray)
}
HStack {
Text("200")
.font(.subheadline)
.bold()
Text("followers")
.font(.caption)
.foregroundColor(.gray)
}
}
.padding(.vertical)
}
.padding(.horizontal)
}
var tweetFilterBar: some View {
HStack {
ForEach(TweetFilterViewModel.allCases, id: \.rawValue) { item in
VStack {
Text(item.title)
.font(.subheadline)
.fontWeight(selectionFilter == item ? .semibold : .regular)
.foregroundColor(selectionFilter == item ? .black : .gray)
ZStack {
Capsule()
.fill(Color(.clear))
.frame(height: 3)
if selectionFilter == item {
Capsule()
.fill(Color(.systemBlue))
.frame(height: 3)
.matchedGeometryEffect(id: "filter", in: animation)
}
}
}
.onTapGesture {
withAnimation(.easeInOut) {
self.selectionFilter = item
}
}
}
}
}
}
While the animation between show and hide is running, the GeometryReader is still calculating values – which lets the view jump between show and hide – and gridlock.
You can introduce a new #State var isInTransition = false that checks if a show/hide animation is in progress and check for that. You set it to true at the beginning of the animation and to false 0.5 secs later.
Also I believe the switch height is not exactly 1/2 of the screen size.
So add a new state var:
#State var isInTransition = false
and in GeometryReader add:
GeometryReader { reader -> AnyView in
let yAxis = reader.frame(in: .global).minY
let height = UIScreen.main.bounds.height / 2
if yAxis < 350 && !isHide && !isInTransition {
DispatchQueue.main.async {
isInTransition = true
withAnimation {
isHide = true
}
}
// wait for animation to finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isInTransition = false
}
} else if yAxis > 0 && isHide && !isInTransition {
DispatchQueue.main.async {
isInTransition = true
withAnimation {
isHide = false
}
}
// wait for animation to finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isInTransition = false
}
}
return AnyView(
// displaying values for test purpose
Text("\(yAxis) - \(isInTransition ? "true" : "false")").foregroundColor(.red)
// .frame(width: 0, height: 0)
)
}

Dropwn list menu open behind of other Views in SwiftUi

When I make dropdown list menu in SwiftUi, dropdown list shows behind of other Component View, I've tried to zIndex(1) at the last of the VStack, and I've tried .overlay at the top of the Stack but it didn't solve my problem, I've shared below code and I've shared screenshot of the problem, how can I solve this problem? thanks...
import SwiftUI
struct DropdownOption: Hashable {
let key: String
let value: String
public static func == (lhs: DropdownOption, rhs: DropdownOption) -> Bool {
return lhs.key == rhs.key
}
}
struct DropdownRow: View {
var option: DropdownOption
var onOptionSelected: ((_ option: DropdownOption) -> Void)?
var body: some View {
Button(action: {
if let onOptionSelected = self.onOptionSelected {
onOptionSelected(self.option)
}
}) {
HStack {
Text(self.option.value)
.font(.system(size: 14))
.foregroundColor(Color.black)
Spacer()
}
}
.padding(.horizontal, 16)
.padding(.vertical, 5)
}
}
struct Dropdown: View {
var options: [DropdownOption]
var onOptionSelected: ((_ option: DropdownOption) -> Void)?
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(self.options, id: \.self) { option in
DropdownRow(option: option, onOptionSelected: self.onOptionSelected)
}
}
}
.frame(minHeight: CGFloat(options.count) * 30, maxHeight: 250)
.padding(.vertical, 5)
.background(Color.white)
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 1)
)
}
}
struct DropdownSelector: View {
#State private var shouldShowDropdown = false
#State private var selectedOption: DropdownOption? = nil
var placeholder: String
var options: [DropdownOption]
var onOptionSelected: ((_ option: DropdownOption) -> Void)?
private let buttonHeight: CGFloat = 45
var body: some View {
Button(action: {
self.shouldShowDropdown.toggle()
}) {
HStack {
Text(selectedOption == nil ? placeholder : selectedOption!.value)
.font(.system(size: 14))
.foregroundColor(selectedOption == nil ? Color.gray: Color.black)
Spacer()
Image(systemName: self.shouldShowDropdown ? "arrowtriangle.up.fill" : "arrowtriangle.down.fill")
.resizable()
.frame(width: 9, height: 5)
.font(Font.system(size: 9, weight: .medium))
.foregroundColor(Color.black)
}
}
.padding(.horizontal)
.cornerRadius(5)
.frame(width: .infinity, height: self.buttonHeight)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 1)
)
.overlay(
VStack {
if self.shouldShowDropdown {
Spacer(minLength: buttonHeight + 10)
Dropdown(options: self.options, onOptionSelected: { option in
shouldShowDropdown = false
selectedOption = option
self.onOptionSelected?(option)
})
}
}, alignment: .topLeading
)
.background(
RoundedRectangle(cornerRadius: 5).fill(Color.white)
)
}
}
calling DropDownList
ZStack(alignment:.top){
HStack {
Group {
DropdownSelector(
placeholder: "Choose Aircraft Type",
options: options,
onOptionSelected: { option in
print(option)
})
.padding(.horizontal)
}
}.padding(.top, 50)
HStack {
Group {
DropdownSelector(
placeholder: "Choose Simulator Type",
options: optionsSimulator,
onOptionSelected: { option in
print(option)
})
.padding(.horizontal)
}
}.padding(.top, 50)
}

HStack in VStack How can I force background color + get rid of dividing lines?

I don't want the behavior I'm getting with this SwiftUI thing (first time messing with it). I've been putting .background() on everything and there's some kind of padding happening and some sort of dividing line, whether I enable the Button code or not (pic below is with Button code commented out).
What do I need to do to fix it?
var body: some View {
ZStack() {
Color.black
.ignoresSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
List(eventFields) { eventField in
HStack() {
Spacer(minLength: 10)
if let iconName = eventField.iconName {
Button(action: {
print("edit \(eventField.name)")
}, label: {
Image(uiImage: UIImage(named: iconName)!.colorizeMask(eventField.iconColor!))
.frame(width: 27, height: 27)
self.background(.black)
}).background(.black)
} else {
Text("")
.frame(width: 27)
}
Text(eventField.iconName == nil ? "" : eventField.name)
.font(.system(size: eventField.labelFontSize))
.foregroundColor(eventField.labelFontColor)
.frame(width: 50, alignment: .trailing)
Spacer(minLength: 3)
Text(eventField.stringValue)
.font(.system(size: eventField.fontSize))
.foregroundColor(eventField.fontColor)
.frame(width: 200, alignment: .leading)
Spacer(minLength: 10)
}.background(.black)
}.background(.black)
}
}
}
}
instead of putting .background on the HStack, use
.listRowBackground(Color.black)
and for separator use
.listRowSeparator(.hidden)
Keep in mind, this is on the HStack not the List
Full Code:
var body: some View {
ZStack() {
Color.black
.ignoresSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
List(eventFields) { eventField in
HStack() {
Spacer(minLength: 10)
if let iconName = eventField.iconName {
Button(action: {
print("edit \(eventField.name)")
}, label: {
Image(uiImage: UIImage(named: iconName)!.colorizeMask(eventField.iconColor!))
.frame(width: 27, height: 27)
self.background(.black)
}).background(.black)
} else {
Text("")
.frame(width: 27)
}
Text(eventField.iconName == nil ? "" : eventField.name)
.font(.system(size: eventField.labelFontSize))
.foregroundColor(eventField.labelFontColor)
.frame(width: 50, alignment: .trailing)
Spacer(minLength: 3)
Text(eventField.stringValue)
.font(.system(size: eventField.fontSize))
.foregroundColor(eventField.fontColor)
.frame(width: 200, alignment: .leading)
Spacer(minLength: 10)
}
.listRowBackground(Color.black)
.listRowSeparator(.hidden)
}
}
}
}
}
I believe the color specification for your hstack and frames is supposed to be "(Color.black)" instead of just "(.black)".
Which type of color you use isn't consistent across all Swift objects. Some objects, such as UITableView use "UI colors" which are in the form ".black", while others, like frames, vstacks, hstacks and other objects, use SwiftUI colors in the form "Color.black".
I recommend this very informative page for a very accessible explanation of using color in a view and a stack.

SwiftUI: Tapping the return key of keyboard erases all the form data

I have forms in my SwiftUI app. In some of them there is a problem relating to keyboard's return key. After editing the form, when I tap the return key to resign the keyboard it erases all the edited data in the form. I could not find any reasonable cause of this problem. I have many network calls in the app.
Here is the code of the login form:
import SwiftUI
struct FormView: View {
var size: CGSize
#State private var errorMessage: String = ""
#State private var isConnectionFailed: Bool = false
#State private var isLoginActive: Bool = false
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
ZStack {
VStack {
VStack {
Image("Logo Registration")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150)
Text("Welcome")
.bold()
.font(.system(size: 22))
.foregroundColor(Color("T1"))
.padding(.top)
Text("Sign in to continue")
.font(.system(size: 18))
.foregroundColor(Color("T1"))
.padding(.top, 8)
}
.padding()
.padding(.bottom)
VStack(spacing: 20) {
HStack {
Image("Call Us_Menu")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35)
.padding(.leading, -10)
Divider()
.rotationEffect(Angle(degrees: 180))
.frame(height: 50)
Text("+88")
.foregroundColor(.gray)
.padding(.horizontal, 10)
Divider()
.rotationEffect(Angle(degrees: 180))
.frame(height: 50)
TextField("Mobile Number", text: self.$viewModel.mobileNumber)
}
.crTextFieldStyle(size: size, height: 50)
HStack {
Image("Password")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35)
.padding(.leading, -10)
Divider()
.rotationEffect(Angle(degrees: 180))
.frame(height: 50)
SecureField("Password", text: self.$viewModel.password)
}
.crTextFieldStyle(size: size, height: 50)
}
HStack {
Text(self.errorMessage)
.font(.system(size: 12))
.foregroundColor(Color("T2"))
Spacer()
}
.frame(width: size.width/1.2)
HStack {
NavigationLink(
destination: ResetPasswordStepOneView()
.navigationBarHidden(isHidden: true)
) {
Text("Forgot Password?")
.foregroundColor(Color("T1"))
}
Spacer()
}
.padding(.bottom, 40)
.frame(width: size.width/1.2)
.font(.system(size: 14))
if self.viewModel.loginModel?.status == "ok" {
NavigationLink(
destination: ContentView()
.navigationBarHidden(isHidden: true)
.navigationBarBackButtonHidden(true),
isActive: $isLoginActive,
label: {
EmptyView()
}
)
}
Button(action: {
if self.viewModel.mobileNumber == "" || self.viewModel.password == "" {
self.errorMessage = "Mobile number or password is missing"
} else if Connectivity.isConnectedToInternet() {
self.viewModel.loading = true
self.errorMessage = ""
self.isLoginActive.toggle()
self.viewModel.fetchWithAF()
} else {
self.isConnectionFailed = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.isConnectionFailed = false
}
}
}) {
Text("SIGN IN")
}
.frame(width: size.width/1.2, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color("B7"), lineWidth: 1)
)
.foregroundColor(Color("T2"))
Text("Or")
.padding()
NavigationLink(
destination: RegistrationView()
.navigationBarHidden(isHidden: true)
) {
Text("CREATE NEW ACCOUNT")
.foregroundColor(Color("T2"))
.bold()
}
Spacer()
}
.frame(width: size.width)
.padding(.bottom, 170)
if self.isConnectionFailed {
ConnectivityError()
}
}
}
}
}
LoginViewModel:
import Foundation
import SwiftUI
import Alamofire
class LoginViewModel: ObservableObject{
#Published var loginModel: LoginModel?
#Published var loading: Bool = false
#Published var isError: Bool = false
#Published var mobileNumber: String = ""
#Published var password: String = ""
#Published var isActive = false
func fetchWithAF() {
let registrationReq = LoginReqModel(phone: "+88" + self.mobileNumber, password: self.password)
let url = AppConstant.signin
let headers: HTTPHeaders = [
"Content-Type": "application/json"
]
AF.request(URL.init(string: url)!, method: .post, parameters: registrationReq, encoder: JSONParameterEncoder.default, headers: headers).responseJSON { (response) in
switch response.result {
case .success(_):
DispatchQueue.main.async {
do {
self.loginModel = try JSONDecoder().decode(LoginModel.self, from: response.data!)
if self.loginModel?.payload?.count ?? 0 > 0 {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(self.loginModel) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: AppConstant.loginPayoad)
}
AppConstant.token = self.loginModel?.payload![0].accessToken ?? ""
}
self.loading = false
if self.loginModel?.status == "ok" {
self.isActive = true
UserDefaults.standard.set(self.mobileNumber, forKey: "Mobile")
UserDefaults.standard.set(self.password, forKey: "Password")
}
if self.loginModel?.status == "error" {
self.isError = true
}
} catch {
print("")
}
}
break
case .failure(let error):
print("working error \(error)")
break
}
}
}
}
Your viewModel is recreated every time the view reloads. Try changing it to:
#StateObject var viewModel = LoginViewModel()
StateObject (introduced in iOS 14) will ensure the object is only created once per view. ObservedObjects need to be owned by some parent view or class.

SwiftUI Animation: How to Move THEN Show?

Example
Here is a menu with three items:
This is fine. But what I would like to achieve is:
Menu expands
Then the three images fade-in AFTER the menu is done expanding
I thought maybe adding a 2nd, delayed animation for the opacity might work but instead, it looks like all animation (movement and opacity) gets delayed:
Here is the code:
struct SequenceAnimation_SOQuestion: View {
#State private var show = false
var body: some View {
HStack(spacing: 40) {
Group {
Image(systemName: "pencil")
Image(systemName: "scribble")
Image(systemName: "lasso")
}
.opacity(show ? 1 : 0)
.animation(Animation.default.delay(0.5))
Button(action: { self.show.toggle() }) {
Image(systemName: "line.horizontal.3.decrease")
.rotationEffect(.degrees(-90))
}.offset(x: 10)
}
.padding(20)
.padding(.leading, 40)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.font(.largeTitle)
.offset(x: show ? -70 : -320)
.animation(.default)
}
}
You should add each animation separately, then you can have different animations for each one:
struct ContentView: View {
#State private var isMenuCollapsed = true
#State private var isItemsVisible = false
var body: some View {
HStack(spacing: 40) {
Group {
Image(systemName: "pencil")
Image(systemName: "scribble")
Image(systemName: "lasso")
}
.opacity(isItemsVisible ? 1 : 0)
Button(action: {
withAnimation(Animation.default) {
self.isMenuCollapsed.toggle()
}
withAnimation(Animation.default.delay(0.2)) {
self.isItemsVisible.toggle()
}
}) {
Image(systemName: "line.horizontal.3.decrease")
.rotationEffect(.degrees(-90))
}.offset(x: 10)
}
.padding(20)
.padding(.leading, 40)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.font(.largeTitle)
.offset(x: isMenuCollapsed ? -320 : -70)
}
}
I don't know exactly what kind of animation you want, but i worked something out which may be somewhere around your goal.
I changed the working of the animations. In your 2nd example your images were animation not right so that's what the following line fixes:
struct SequenceAnimation_SOQuestion: View {
#State private var show = false
var body: some View {
HStack(spacing: 40) {
Group {
Image(systemName: "pencil")
Image(systemName: "scribble")
Image(systemName: "lasso")
}
.opacity(show ? 1 : 0)
.animation(!self.show ? .default : Animation.default.delay(0.5))
Button(action: { self.show.toggle() }) {
Image(systemName: "line.horizontal.3.decrease")
.rotationEffect(.degrees(-90))
}.offset(x: 10)
}
.padding(20)
.padding(.leading, 40)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.font(.largeTitle)
.offset(x: show ? -70 : -320)
.animation(!self.show ? Animation.default.delay(0.5) : .default)
}
}
If you provide me a bit more specific information about your goal you're trying to achieve, I can maybe help you.