I want to move the view when I log in successfully - swiftui

Button {
if(UserApi.isKakaoTalkLoginAvailable()){
UserApi.shared.loginWithKakaoTalk { (oauthToken, error) in
print(oauthToken)
print(error)
}
} else {
UserApi.shared.loginWithKakaoAccount {(oauthToken, error) in
print(oauthToken)
print(error)
}
}
UserApi.shared.loginWithKakaoAccount(prompts: [.Login]) {(oauthToken, error) in
if let error = error {
print(error)
} else {
print("LoginSuccess")
MainView()// I want to go to this view
_ = oauthToken
}
}
}
I tried navigation, but it didn't work
I want to call View when Token is here
Token type is Bool

you could try this:
#State var showMainView = false // <-- here
if showMainView { // <-- here
MainView() // I want to go to this view
} else {
Button {
if(UserApi.isKakaoTalkLoginAvailable()){
UserApi.shared.loginWithKakaoTalk { (oauthToken, error) in
print(oauthToken)
print(error)
}
} else {
UserApi.shared.loginWithKakaoAccount {(oauthToken, error) in
print(oauthToken)
print(error)
}
}
UserApi.shared.loginWithKakaoAccount(prompts: [.Login]) {(oauthToken, error) in
if let error = error {
print(error)
} else {
print("LoginSuccess")
self.showMainView = true // <-- here
// or DispatchQueue.main.async { self.showMainView = true }
_ = oauthToken
}
}
}
}
You should also read this: https://developer.apple.com/tutorials/swiftui/
Note, it would be better to do all these UserApi stuff in a separate dedicated class, rather than inside the Button.
EDIT-1: you can also use NavigationLink, for example:
struct ContentView: View {
#State var showMainView = false
var body: some View {
NavigationView {
VStack {
Button("Login") { doAPIStuff() }
NavigationLink("", destination: MainView(), isActive: $showMainView)
}
}
}
func doAPIStuff() {
if(UserApi.isKakaoTalkLoginAvailable()){
UserApi.shared.loginWithKakaoTalk { (oauthToken, error) in
print(oauthToken)
print(error)
}
} else {
UserApi.shared.loginWithKakaoAccount {(oauthToken, error) in
print(oauthToken)
print(error)
}
}
UserApi.shared.loginWithKakaoAccount(prompts: [.Login]) {(oauthToken, error) in
if let error = error {
print(error)
} else {
print("LoginSuccess")
DispatchQueue.main.async { self.showMainView = true } // <-- here
_ = oauthToken
}
}
}
}

Related

How to display an Error Alert in SwiftUI?

Setup:
I have a SwiftUI View that can present alerts. The alerts are provided by an AlertManager singleton by setting title and/or message of its published property #Published var nextAlertMessage = ErrorMessage(title: nil, message: nil). The View has a property #State private var presentingAlert = false.
This works when the following modifiers are applied to the View:
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
Problem:
Since alerts are also to be presented in other views, I wrote the following custom view modifier:
struct ShowAlert: ViewModifier {
#Binding var presentingAlert: Bool
let alertManager = AlertManager.shared
func body(content: Content) -> some View {
return content
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
and applied it to the View as:
.modifier(ShowAlert(presentingAlert: $presentingAlert))
However, no alerts are now shown.
Question:
What is wrong with my code and how to do it right?
Edit (as requested by Ashley Mills):
Here is a minimal reproducible example.
Please note:
In ContentView, the custom modifier ShowAlert has been out commented. This version of the code shows the alert.
If instead the modifiers .onAppear, .onChange and .alert are out commented, and the custom modifier is enabled, the alert is not shown.
// TestViewModifierApp
import SwiftUI
#main
struct TestViewModifierApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var alertManager = AlertManager.shared
#State private var presentingAlert = false
var body: some View {
let alertManager = AlertManager.shared
let _ = alertManager.showNextAlertMessage(title: "Title", message: "Message")
Text("Hello, world!")
// .modifier(ShowAlert(presentingAlert: $presentingAlert))
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
// AlertManager
import SwiftUI
struct ErrorMessage: Equatable {
let title: String?
let message: String?
var joinedTitle: String {
(title ?? "") + "\n\n" + (message ?? "")
}
static func == (lhs: ErrorMessage, rhs: ErrorMessage) -> Bool {
lhs.title == rhs.title && lhs.message == rhs.message
}
}
final class AlertManager: NSObject, ObservableObject {
static let shared = AlertManager() // Instantiate the singleton
#Published var nextAlertMessage = ErrorMessage(title: nil, message: nil)
func showNextAlertMessage(title: String?, message: String?) {
DispatchQueue.main.async {
// Publishing is only allowed from the main thread
self.nextAlertMessage = ErrorMessage(title: title, message: message)
}
}
func alertConfirmed() {
showNextAlertMessage(title: nil, message: nil)
}
}
// ShowAlert
import SwiftUI
struct ShowAlert: ViewModifier {
#Binding var presentingAlert: Bool
let alertManager = AlertManager.shared
func body(content: Content) -> some View {
return content
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
You're over complicating this, the way to present an error alert is as follows:
Define an object that conforms to LocalizedError. The simplest way to do it is an enum, with a case for each error your app can encounter. You have to implement var errorDescription: String?, this is displayed as the alert title. If you want to display an alert message, then add a method to your enum to return this.
enum MyError: LocalizedError {
case basic
var errorDescription: String? {
switch self {
case .basic:
return "Title"
}
}
var errorMessage: String? {
switch self {
case .basic:
return "Message"
}
}
}
You need a #State variable to hold the error and one that's set when the alert should be presented. You can do it like this:
#State private var error: MyError?
#State private var isShowingError: Bool
but then you have two sources of truth, and you have to remember to set both each time. Alternatively, you can use a computed property for the Bool:
var isShowingError: Binding<Bool> {
Binding {
error != nil
} set: { _ in
error = nil
}
}
To display the alert, use the following modifier:
.alert(isPresented: isShowingError, error: error) { error in
// If you want buttons other than OK, add here
} message: { error in
if let message = error.errorMessage {
Text(message)
}
}
4. Extra Credit
As you did above, we can move a bunch of this stuff into a ViewModifier, so we end up with:
enum MyError: LocalizedError {
case basic
var errorDescription: String? {
switch self {
case .basic:
return "Title"
}
}
var errorMessage: String? {
switch self {
case .basic:
return "Message"
}
}
}
struct ErrorAlert: ViewModifier {
#Binding var error: MyError?
var isShowingError: Binding<Bool> {
Binding {
error != nil
} set: { _ in
error = nil
}
}
func body(content: Content) -> some View {
content
.alert(isPresented: isShowingError, error: error) { _ in
} message: { error in
if let message = error.errorMessage {
Text(message)
}
}
}
}
extension View {
func errorAlert(_ error: Binding<MyError?>) -> some View {
self.modifier(ErrorAlert(error: error))
}
}
Now to display an error, all we need is:
struct ContentView: View {
#State private var error: MyError? = .basic
var body: some View {
Text("Hello, world!")
.errorAlert($error)
}
}

data from ObservableObject class do not pass to .alert()

Sorry for simple question, try to learn SwiftUI
My goal is to show alert then i can not load data from internet using .alert()
the problem is that my struct for error actually has data but it does not transfer to .alert()
debug shows that AppError struct fill in with error but then i try to check for nil or not it is always nil in .Appear()
PostData.swift
struct AppError: Identifiable {
let id = UUID().uuidString
let errorString: String
}
NetworkManager.swift
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
#Published var appError: AppError? = nil
func fetchGuardData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decorder = JSONDecoder()
if let safeData = data {
do {
let results = try decorder.decode(Results.self, from: safeData)
DispatchQueue.main.sync {
self.posts = results.hits }
} catch {
self.appError = AppError(errorString: error.localizedDescription)
}
} else {
self.appError = AppError(errorString: error!.localizedDescription)
}
} else {
DispatchQueue.main.sync {
self.appError = AppError(errorString: error!.localizedDescription)
}
}
} //
task.resume()
} else {
self.appError = AppError(errorString: "No url response")
}
}
}
ContentView.swift
struct ContentView: View {
#StateObject var networkManager = NetworkManager()
#State var showAlert = false
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4NEWS")
}
.onAppear() {
networkManager.fetchGuardData()
if networkManager.appError != nil {
showAlert = true
}
}
.alert(networkManager.appError?.errorString ?? "no data found", isPresented: $showAlert, actions: {})
}
}
Probably when doing this check, the data fetch process is not finished yet.
if networkManager.appError != nil {
showAlert = true
}
So you should wait the network request finish to check if there is error or not.
If you sure there is error and just test this try this to see error:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if networkManager.appError != nil {
showAlert = true
}
}
To handle better this situation you can pass a closure your fetchGuardData function and handle your result and error inside it.
or you can use .onChange for the listen the changes of appError.
.onChange(of: networkManager.appError) { newValue in }

#EnvironmentVariable not being passed to child views

I have 2 views - which I want to navigate between, and have a viewModel object shared between them as an EnvironmentObject. I keep getting the "A View.environmentObject(_:) for TidesViewModel may be missing as an ancestor of this view." error - but none of the solutions I have found seem to work. Please find below my code. The following is the first view:
import SwiftUI
struct ContentView: View {
#ObservedObject var tidesViewModel: TidesViewModel = TidesViewModel()
var body: some View {
NavigationView
{
List
{
ForEach (tidesViewModel.stations.indices) {
stationid in
HStack
{
NavigationLink(destination: TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id))
{
Text(tidesViewModel.stations[stationid].properties.Name)
}
}
}
}
}.environmentObject(tidesViewModel)
}
}
and below is the child view - which throws the error.
import SwiftUI
struct TideDataView: View {
#EnvironmentObject var tidesViewModel : TidesViewModel
var stationId: String
init(stationId: String) {
self.stationId = stationId
getTidesForStation(stationId: stationId)
}
var body: some View {
List
{
ForEach (tidesViewModel.tides.indices)
{
tideIndex in
Text(tidesViewModel.tides[tideIndex].EventType)
}
}
}
func getTidesForStation(stationId: String)
{
tidesViewModel.getTidalData(forStation: stationId)
}
}
For completeness - below is the Observable object being passed:
import Foundation
import SwiftUI
class TidesViewModel: ObservableObject
{
private var tideModel: TideModel = TideModel()
var currentStation: Feature?
init()
{
readStations()
}
var stations: [Feature]
{
tideModel.features
}
var tides: [TidalEvent]
{
tideModel.tides
}
func readStations()
{
let stationsData = readLocalFile(forName: "stations")
parseStations(jsonData: stationsData!)
}
private func readLocalFile(forName name: String) -> Data? {
do {
if let bundlePath = Bundle.main.path(forResource: name,
ofType: "json"),
let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
return jsonData
}
} catch {
print(error)
}
return nil
}
private func parseStations(jsonData: Data) {
do {
let decodedData: FeatureCollection = try JSONDecoder().decode(FeatureCollection.self,
from: jsonData)
//print(decodedData)
tideModel.features = decodedData.features
} catch let jsonError as NSError{
print(jsonError.userInfo)
}
}
func getTidalData(forStation stationId: String)
{
let token = "f43c068141bb417fb88909be5f68781b"
guard let url = URL(string: "https://admiraltyapi.azure-api.net/uktidalapi/api/V1/Stations/" + stationId + "/TidalEvents") else {
fatalError("Invalid URL")
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue(token, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else{ return }
do{
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let decodedData = try decoder.decode([TidalEvent].self, from: data)
DispatchQueue.main.async {
self.tideModel.tides = decodedData
}
}catch let error{
print(error)
}
}.resume()
}
}
You need to attach the modifier .environmentObject() directly to the view that will receive it. So, in your case, attach it to TideDataView, not to the NavigationView around it.
Your code would look like this:
NavigationLink {
TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id)
.environmentObject(tidesViewModel)
} label: {
Text(tidesViewModel.stations[stationid].properties.Name)
}
// Delete the modifier .environmentObject() attached to the NavigationView

Confirm from model in SwiftUI

let us imagine that I have something like the following core/model:
class Core: ObservableObject {
...
func action(confirm: () -> Bool) {
if state == .needsConfirmation, !confirm() {
return
}
changeState()
}
...
}
and then I use this core object in a SwiftUI view.
struct ListView: View {
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
core.action {
// present an alert to the user and return if the user confirms or not
}
}
}
}
}
So boiling it down, I wonder how to work with handlers there need an input from the user, and I cant wrap my head around it.
It looks like you reversed interactivity concept, instead you need something like below (scratchy)
struct ListView: View {
#State private var confirmAlert = false
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
if core.needsConfirmation {
self.confirmAlert = true
} else {
self.core.action() // << direct action
}
}
}
.alert(isPresented: $confirmAlert) {
Alert(title: Text("Title"), message: Text("Message"),
primaryButton: .default(Text("Confirm")) {
self.core.needsConfirmation = false
self.core.action() // <<< confirmed action
},
secondaryButton: .cancel())
}
}
}
class Core: ObservableObject {
var needsConfirmation = true
...
func action() {
// just act
}
...
}
Alternate: with hidden condition checking in Core
struct ListView: View {
#ObservedObject core: Core
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
self.core.action() // << direct action
}
}
.alert(isPresented: $core.needsConfirmation) {
Alert(title: Text("Title"), message: Text("Message"),
primaryButton: .default(Text("Confirm")) {
self.core.action(state: .confirmed) // <<< confirmed action
},
secondaryButton: .cancel())
}
}
}
class Core: ObservableObject {
#Published var needsConfirmation = false
...
func action(state: State = .check) {
if state == .check && self.state != .confirmed {
self.needsConfirmation = true
return;
}
self.state = state
// just act
}
...
}

How to dismiss swiftUI from Viewcontroller?

In my an viewController I have this function
...{
let vc = UIHostingController(rootView: SwiftUIView())
present(vc, animated: true, completion: nil)
}
Which present the following SwiftUIView.
Q How to dismiss the SwiftUIView when CustomButton pressed?
struct SwiftUIView : View {
var body: some View {
CustomButton()
}
}
struct CustomButton: View {
var body: some View {
Button(action: {
self.buttonAction()
}) {
Text(buttonTitle)
}
}
func buttonAction() {
//dismiss the SwiftUIView when this button pressed
}
}
struct CustomButton: View {
var body: some View {
Button(action: {
self.buttonAction()
}) {
Text(buttonTitle)
}
}
func buttonAction() {
if let topController = UIApplication.topViewController() {
topController.dismiss(animated: true)
}
}
}
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
Or if this doesn't work because you have a different view controller on top or if you need to use view life cycle events (onDisappear and onAppear won't work with UIHostingController).
You can use instead:
final class SwiftUIViewController: UIHostingController<CustomButton> {
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: CustomButton())
rootView.dismiss = dismiss
}
init() {
super.init(rootView: CustomButton())
rootView.dismiss = dismiss
}
func dismiss() {
dismiss(animated: true, completion: nil)
}
override func viewWillDisappear(_ animated: Bool) {
rootView.prepareExit()
}
override func viewDidDisappear(_ animated: Bool) {
rootView.doExit()
}
}
struct CustomButton: View {
var dismiss: (() -> Void)?
var body: some View {
Button(action: dismiss! ) {
Text("Dismiss")
}
}
func prepareExit() {
// code to execute on viewWillDisappear
}
func doExit() {
// code to execute on viewDidDisappear
}
}