How to change language of the app with toggles in SwiftUI - swift

I have a view with list of toggles with languages that should change current app language after the restart. I understand how to create this feature with buttons, but it doesn't work the same way with toggles. I've tried to use on change, but it's not allowing to turn off the toggle after the second tap. How to do that properly?
struct Languages: View {
#State private var currentLanguage = true
#State private var currentLanguageEnglish = true
#State private var currentLanguageRussian = false
#State private var showingAlert = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#State var currentSysLanguage = UserDefaults.standard.string(forKey: "language")
var body: some View {
VStack(alignment: .leading) {
DoubleTextView(topText: LocalizedStringKey("languages"), buttomText: "", topTextSize: 24, buttomTextSize: 0)
// Works just right, wrong design.
Button("English", action: {
currentSysLanguage = "en"
UserDefaults.standard.set(currentSysLanguage, forKey: "language")
showingAlert.toggle()
})
.alert("Restart your app", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
Button("French", action: {
currentSysLanguage = "fr"
UserDefaults.standard.set(currentSysLanguage, forKey: "language")
showingAlert.toggle()
})
.alert("Restart your app", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
// Does not work, this is how it should be.
ZStack(alignment: .top) {
Toggle(isOn: $currentLanguageEnglish) {
Text("English")
.font(.custom("Manrope-Bold", size: 16))
.foregroundColor(.white)
}
.padding(.horizontal)
.tint(Color("active"))
}
.padding(.vertical, 30.0)
.background(Rectangle()
.fill(Color("navigation"))
.frame(height: 50)
.cornerRadius(8))
ZStack(alignment: .top) {
Toggle(isOn: $currentLanguageRussian) {
Text("French")
.font(.custom("Manrope-Bold", size: 16))
.foregroundColor(.white)
}
.padding(.horizontal)
.tint(Color("active"))
}
.background(Rectangle()
.fill(Color("navigation"))
.frame(height: 50)
.cornerRadius(8))
Spacer()
}
.background(
Image("background2")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
)
.onChange(of: currentLanguageEnglish, perform: { newValue in
currentLanguageRussian = true
})
.onChange(of: currentLanguageRussian, perform: { newValue in
currentLanguageEnglish = true
})
.padding(.top, 40)
.overlay {
HStack {
Spacer()
Button {
self.mode.wrappedValue.dismiss()
} label: {
HStack {
Image(systemName: "arrow.left")
.foregroundColor(.white)
.font(.system(size: 24))
Spacer()
}
}
}
.padding(.horizontal, 3.0)
.frame(maxHeight: .infinity, alignment: .top)
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
.padding(.horizontal, 10.0)
}
}

There are many ways you can accomplish something similar, one is to make the toggle bind to a custom binding object:
Toggle(isOn: .init(get: {
currentSysLanguage == "en"
}, set: { isOn in
currentSysLanguage = isOn ? "en" : defaultLanguage
})) {
Text("English")
}
Here the getter makes the toggle listen to currentSysLanguage == "en". It will be on/off based on if the statement evaluates true. Whereas the setter will be triggered when you manually toggle it, and set currentSysLanguage between "en" and defaultLanguage.
Note I have added the defaultLanguage to represent the state when all toggles are off.
You can then create as many toggles as needed with the same code by changing the language code it is comparing to.
Near the end, you would add a .onChange modifier to listen to currentSysLanguage to send the changes to UserDefaults:
.onChange(of: currentSysLanguage, perform: { newValue in
UserDefaults.standard.set(newValue, forKey: "language")
})

Related

Swift: reload view after midnight or when a widget refreshes?

My app uses CoreData to store one quote for each day. There's a widget, the timeline of which is set to update after midnight and fetch the quote for that particular date. This works fine. However, I noticed that when I click on the widget with the new day's quote, the app - which has remained in the background - still shows the previous day's quote. Removing the app from the list of open ones and reopening it shows the correct information.
Is there a way to force a refresh of the DailyQuoteView and make sure that there isn't leftover data?
EDIT: Here's the daily quote struct that I'd like to always show the relevant data when brought from the background.
import SwiftUI
struct DailyQuoteView: View {
#Environment(\.scenePhase) var scenePhase
#State var id: UUID = .init()
#EnvironmentObject var unsplashModel: UnsplashModel
let managedObjectContext = CoreDataStack.shared.managedObjectContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)],
predicate: NSPredicate(
format: "date >= %# AND date <= %#",
argumentArray: [
Calendar.current.startOfDay(for: Date()),
Calendar.current.date(byAdding: .minute, value: 1439, to: Calendar.current.startOfDay(for: Date()))!
]
)
) var quote: FetchedResults<QuoteEntity>
#FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
) var quotes: FetchedResults<QuoteEntity>
#State var shouldPresentSheet = false
#State var animateCard = false
#Binding var cardOffset: Double
#Binding var coverImageSize: Double
#State var showAboutScreen = false
// MARK: - ELEMENTS
var coverImage: some View {
Image(uiImage: UIImage(data: quote.first?.image ?? Data()) ?? UIImage())
.resizable()
.scaledToFill()
}
var aboutButton: some View {
Button {
showAboutScreen.toggle()
} label: {
Image(systemName: "info")
.foregroundColor(.primary)
.padding(10)
.background(Circle().fill(Color("background")))
.font(.title3)
}
.padding()
.shadow(radius: 5)
}
var titleView: some View {
Text(quote.first?.title ?? "")
.font(.largeTitle)
.multilineTextAlignment(.center)
.bold()
.minimumScaleFactor(0.5)
}
var contentsBox: some View {
ScrollView {
VStack (alignment: .leading, spacing: 10) {
Text(quote.first?.summary ?? "")
.font(.body)
.multilineTextAlignment(.leading)
.padding(.bottom, 20)
if let username = quote.first?.imageUserName {
HStack(spacing: 0) {
Text("Photo: ")
Link(
destination: URL(string: "https://unsplash.com/#\(username)?utm_source=quote_of_the_day&utm_medium=referral")!,
label: {
Text((quote.first?.imageUser)!)
.underline()
})
Text(" on ")
Link(
destination: URL(string: "https://unsplash.com/")!,
label: {
Text("Unsplash")
.underline()
})
Spacer()
}
.font(.caption)
.italic()
.foregroundColor(.gray)
.padding(.vertical, 3)
}
}
.font(.subheadline)
}
}
var buttonRow: some View {
HStack {
Menu(
content: {
Button {
shouldPresentSheet.toggle()
} label: {
Text("as image")
Image(systemName: "photo")
}
ShareLink(item: quote.first!.title, message: Text(quote.first!.summary)) {
Text("as text")
Image(systemName: "text.justify.left")
}
},
label: {
HStack {
Image(systemName: "square.and.arrow.up")
Text("Share")
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).fill(Color("whitesand")))
.tint(Color(.secondaryLabel))
}
)
Spacer()
Link(destination: URL(string: quote.first!.link)!) {
Image(systemName: "book")
Text("Read more")
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).fill(Color("vpurple")))
.tint(Color(.white))
}
.frame(height: 60)
.sheet(isPresented: $shouldPresentSheet) {
QuoteShare(quote: quote.first!)
}
}
var cardView: some View {
Rectangle()
.fill((Color("background")))
.cornerRadius(50, corners: [.topLeft, .topRight])
.overlay{
VStack{
titleView
.padding(.top, 30)
contentsBox
Spacer()
buttonRow
}
.padding(.horizontal, 20)
.frame(maxWidth: 600)
}
}
// MARK: - BODY
var body: some View {
ZStack{
GeometryReader{ proxy in
coverImage
.ignoresSafeArea()
.frame(width: UIScreen.main.bounds.width, height: proxy.size.height * (animateCard ? 0.30 : 1.0))
.animation(.easeInOut(duration: 1), value: animateCard)
}
GeometryReader { proxy in
aboutButton
.frame(width: 30, height: 30)
.position(x: proxy.frame(in: .global).maxX - 30, y: proxy.frame(in: .global).minY)
}
GeometryReader{ proxy in
VStack(spacing: 0){
Spacer()
cardView
.frame(height: proxy.size.height * 0.8)
.offset(y: animateCard ? 0 : cardOffset)
.animation(.spring().delay(0.2), value: animateCard)
}
}
}
.id(id)
.blur(radius: shouldPresentSheet ? 5.0 : 0.0)
.brightness(shouldPresentSheet ? -0.2 : 0.0)
.animation(.easeIn, value: shouldPresentSheet)
.onAppear {
if !imagesLoaded {
unsplashModel.loadData(imageCount: quotes.count)
imagesLoaded = true
}
animateCard = true
}
.onChange(of: unsplashModel.dailyImageData) { _ in
if unsplashModel.dailyImageData.count == quotes.count {
updateDailyImages()
}
}
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
id = .init()
}
})
.task {
NotificationManager.shared.scheduleNotifications(notificationText: quote.first?.title ?? "")
}
}
}
EDIT 2: Code updated with lorem ipsum's suggestion below (.id and onChange of scenePhase). However, this seems to interfere with the card animation in .onAppear { withAnimation { animateCard = true } }

Can't solve scroll view in SwiftUI

Could someone please explain to me how the ScrollView works, how can I make that when I receive a message it scrolls to the last message ( item )?
I tried to make it with a, ScrollViewReader { proxy in } but I still can't make this code work.
I tried also to make it ' .onchange(of: ) { } ' (with ScrollViewReader)
struct ContentView: View{
#ObservedObject var viewModel = ViewModel()
#State var text = ""
#State var models = [String]()
var body: some View {
VStack(alignment: .leading) {
ScrollViewReader { proxy in
ScrollView (showsIndicators: false){
ForEach(models, id: \.self){ string in
Text(string)
.font(.headline)
.padding()
.background(Color("Text"))
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(.blue, style: .init(lineWidth: 1))
)
Divider()
}
.onChange(of: models){
proxy.scrollTo(models.last)
}
}
}
Spacer()
HStack{
TextField("Type here...", text: $text)
.padding(15.0)
.padding(.horizontal)
Button(){
send()
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 40))
.foregroundColor(Color("Button"))
}
.padding(10.0)
}
.overlay(
RoundedRectangle(cornerRadius: 100)
.stroke(Color("Stroke"), lineWidth: 1)
)
}
.onAppear{
viewModel.setup()
}
.padding()
.background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color("Background")/*#END_MENU_TOKEN#*/)
}
func send() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
models.append("Me: \(self.text)")
viewModel.send(text: text) { response in
DispatchQueue.main.async {
self.models.append("Answer: "+response)
self.text = ""
}
}
}
}
..............................
use a different overload
proxy.scrollTo(models.last,anchor:.top)

How to perform action before executing navigationLink

I am building and app and I am currently in the Login view. In this view, the user enters its information and then clicks a "Log In" button. The thing is that I need to make some calls to my API when the user clicks the button, and then execute the NavigationLink method, if the data the user entered was correct. The only way I have managed to to this has been using isActive parameter, but it is no longer available as it is deprecated.
My code currently is the following:
var body: some View {
NavigationView(){
GeometryReader { geometry in
VStack(alignment: .center, spacing: 24){
TextField("Enter your username", text: $username)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
SecureField("Enter your password", text: $password)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
Text("Log in")
.font(.title2)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Color.accent)
.cornerRadius(22)
.padding(.horizontal).onTapGesture {
buttonClicked()
// In the method buttonClicked I would like to make the API
// calls and then the navigate to main view
}
}
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
.background(Color.welcomeBackground)
.navigationTitle("Log In")
}
}
.toolbar(.visible)
}
I have also tried with NavigationStack, but it doesn't work either.
#State private var signInPressed: Bool = false
var body: some View {
NavigationStack{
GeometryReader { geometry in
VStack(alignment: .center, spacing: 24){
TextField("Enter your username", text: $username)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
SecureField("Enter your password", text: $password)
.font(.title3)
.frame(maxWidth: geometry.size.width*0.85)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(22)
Text("Log in")
.font(.title2)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Color.accent)
.cornerRadius(22)
.padding(.horizontal).onTapGesture {
signInPressed.toggle()
}
}
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
.background(Color.welcomeBackground)
.navigationTitle("Log In")
}
}
.navigationDestination(isPresented: $signInPressed, destination: {
ContentView()
})
.toolbar(.visible)
}
Instead of using a navigation link use a button and have a separate navigation link which can be activate by a #State variable. I've added a fake login method that disables the button and shows an activity indicator while the login process in occurring.
< iOS 16:
func Login() async -> Bool {
try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) // 5 seconds
return true
}
struct LoginView : View {
#State private var showMainView = false
#State private var isLoggingIn = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: Text("Logged in"), isActive: $showMainView) { EmptyView() }
Button(action: {
isLoggingIn = true
// do code to validate login here
Task {
//if login call worked then show the main view
showMainView = await Login()
isLoggingIn = false
}
})
{
HStack{
Spacer()
if(isLoggingIn){
ProgressView()
}
else{
Text("Login")
}
Spacer()
}.padding()
.background(.green)
}.disabled(isLoggingIn)
}
}
}
}
iOS 16:
In iOS 16 this can be replaced by using a NavigationStack and a navigationDestination as seen below.
struct LoginView : View {
#State private var showMainView = false
#State private var isLoggingIn = false
var body: some View {
NavigationStack{
VStack{
Button(action: {
isLoggingIn = true
// do code to validate login here
Task {
//if login call worked then show the main view
showMainView = await Login()
isLoggingIn = false
}
})
{
HStack{
Spacer()
if(isLoggingIn){
ProgressView()
}
else{
Text("Login")
}
Spacer()
}.padding()
.background(.green)
}.disabled(isLoggingIn)
}.navigationDestination(isPresented: $showMainView) {
Text("Logged in")
}
}
}
}

SwiftUI button glitches on keyboard dismiss

I have a button on my view that, when pressed, calls a hideKeyboard function which is the following:
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
However, when the button is pressed and the keyboard gets dismissed, a glitch occurs where the button stays in place while the view moves downward:
https://giphy.com/gifs/Z7qCDpRSGoOb9CbVRQ
Reproducible example:
import SwiftUI
struct TestView: View {
#State private var username: String = ""
#State private var password: String = ""
#State private var isAnimating: Bool = false
var lightGrey = Color(red: 239.0/255.0,
green: 243.0/255.0,
blue: 244.0/255.0)
var body: some View {
ZStack() {
VStack() {
Spacer()
Text("Text")
.font(.title)
.fontWeight(.semibold)
.padding(.bottom, 15)
.frame(maxWidth: .infinity, alignment: .leading)
Text("Text")
.font(.subheadline)
.padding(.bottom)
.frame(maxWidth: .infinity, alignment: .leading)
TextField("username", text: $username)
.padding()
.background(self.lightGrey)
.cornerRadius(5.0)
.padding(.bottom, 20)
SecureField("password", text: $password)
.padding()
.background(self.lightGrey)
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: {
self.hideKeyboard()
login()
})
{
if isAnimating {
ProgressView()
.colorScheme(.dark)
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
else {
Text("Text")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
}
.disabled(username.isEmpty || password.isEmpty || isAnimating)
Text("Text")
.font(.footnote)
.frame(maxWidth: .infinity, alignment:.leading)
Spacer()
}
.padding()
.padding(.bottom, 150)
.background(Color.white)
}
}
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
func login() {
isAnimating = true
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
This is due to view replacement inside button, find below a fixed re-layout.
Tested with Xcode 13.2 / iOS 15.2 (slow animation was activated for demo)
Button(action: {
self.hideKeyboard()
login()
})
{
VStack { // << persistent container !!
if isAnimating {
ProgressView()
.colorScheme(.dark)
}
else {
Text("Text")
}
}
.foregroundColor(.white)
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
.disabled(username.isEmpty || password.isEmpty || isAnimating)
Looks like a conflict between UIApplication.shared.sendAction and SwiftUI: hiding the keyboard starts the animation, but updating the isAnimating property resets it.
Changing the order of calls solves the problem:
Button(action: {
login()
self.hideKeyboard()
})

How to align the an image on top of a button in swiftui?

I wish to add a 'trash' image on the top-right side of each button when 'Delete Button' is pressed, so that when user hits the trash image, the button will be removed from the vstack.
I think I should use zstack to position the trash image but I don't know how for now.
Below shows where the trash image should be located in each button.
Also, when I press the 'Delete Button', it seems that each button's text size and spacing with another button is changed slightly. How do I overcome this problem? The button position, spacing, textsize should be unchanged when 'Delete Button' is hit.
struct someButton: View {
#Environment(\.editMode) var mode
#ObservedObject var someData = SomeData()
#State var newButtonTitle = ""
#State var isEdit = false
var body: some View {
NavigationView{
// List{ // VStack
VStack{
VStack{
ForEach(Array(someData.buttonTitles.keys.enumerated()), id: \.element){ ind, buttonKeyName in
//
Button(action: {
self.someData.buttonTitles[buttonKeyName] = !self.someData.buttonTitles[buttonKeyName]!
print("Button pressed! buttonKeyName is: \(buttonKeyName) Index is \(ind)")
print("bool is \(self.someData.buttonTitles[buttonKeyName]!)")
}) {
HStack{ //HStack, ZStack
if self.isEdit{
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture{
print("buttonkey \(buttonKeyName) will be deleted")
self.deleteItem(ind: ind)
}
}
Text(buttonKeyName)
// .fontWeight(.semibold)
// .font(.title)
}
}
.buttonStyle(GradientBackgroundStyle(isTapped: self.someData.buttonTitles[buttonKeyName]!))
.padding(.bottom, 20)
}
}
HStack{
TextField("Enter new button name", text: $newButtonTitle){
self.someData.buttonTitles[self.newButtonTitle] = false
self.newButtonTitle = ""
}
}
}
.navigationBarItems(leading: Button(action: {self.isEdit.toggle()}){Text("Delete Button")},
trailing: EditButton())
// .navigationBarItems(leading: Button(action: {}){Text("ergheh")})
// }
}
}
func deleteItem(ind: Int) {
let key = Array(someData.buttonTitles.keys)[ind]
print(" deleting ind \(ind), key: \(key)")
self.someData.buttonTitles.removeValue(forKey: key)
}
}
struct GradientBackgroundStyle: ButtonStyle {
var isTapped: Bool
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, maxHeight: 50)
.padding()
.foregroundColor(isTapped ? Color.blue : Color.black)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(isTapped ? Color.blue : Color.black, lineWidth: 4))
.shadow(radius: 40)
.padding(.horizontal, 20)
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
//
}
}
class SomeData: ObservableObject{
#Published var buttonTitles: [String: Bool] = ["tag1": false, "tag2": false]
}
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4 (with some replicated code)
var body: some View {
Button(action: { }) {
Text("Name")
}
.buttonStyle(GradientBackgroundStyle(isTapped: tapped))
.overlay(Group {
if self.isEdit {
ZStack {
Button(action: {print(">> Trash Tapped")}) {
Image(systemName: "trash")
.foregroundColor(.red).font(.title)
}.padding(.trailing, 40)
.alignmentGuide(.top) { $0[.bottom] }
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
}
})
.padding(.bottom, 20)
}