How can I create SignIn and SignOut with 1 button in RxSwift? - mvvm

Here my code to change text in button:
func transform(input: Input) -> Output {
let tappedSigninCheck = input.signinTrigger
.scan(false) { lastState, _ in
return !lastState }
let singupButtonTitle = tappedSigninCheck.map {
return $0 == true ? "Sign Out" : "Sign In"
}
return Output(signinButtonTitle: singupButtonTitle)
}
Now when I clicked SignIn, button text will change to "Sign Out" and I want when click Sign Out, sign out will display 1 alert and when click Yes in alert then button text change to Sign In.
My problem: whenever I click in button then button text change text :(((
func bindViewModel() {
let signoutTrigger = signinButton.rx.tap.flatMap {
return Observable<Void>.create { (observer) -> Disposable in
let alert = UIAlertController(title: "Are you sure to sign out?",
message: nil,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "No",
style: .cancel,
handler: nil))
alert.addAction(UIAlertAction(title: "Yes",
style: .destructive,
handler: { _ in
observer.onNext(Void())
}))
self.present(alert, animated: true)
return Disposables.create()
}
}
let input = ProfileViewModel.Input(signinTrigger: signinButton.rx.tap.asDriver(),
signoutTrigger: signoutTrigger.asDriver(onErrorJustReturn: Void()))
let output = profileViewModel.transform(input: input)
output
.signinButtonTitle
.drive(signinButton.rx.title)
.disposed(by: disposeBag)
}

For this you will need a feedback loop, and that calls for a Subject. I suggest you do it something like below. Also note that you forgot to emit a completed event on your alert.:
class ViewModel {
struct Input {
let logButton: Observable<Void>
let okayButton: Observable<Void>
}
struct Output {
let buttonTitle: Observable<String>
let displayAlert: Observable<Void>
}
private enum Action {
case tapped
case okay
}
private enum State {
case offline
case online
case check
}
func transform(_ input: Input) -> Output {
let state = Observable.merge(
input.logButton.map(to: ViewModel.Action.tapped),
input.okayButton.map(to: ViewModel.Action.okay)
)
.scan(ViewModel.State.offline) { state, action in
switch (state, action) {
case (.offline, .tapped):
return .online
case (.online, .tapped):
return .check
case (.check, .okay):
return .offline
case (.check, .tapped):
return .check
default:
assert(false)
return state
}
}
.share()
let buttonTitle = state
.map { $0 == .offline ? "Log In" : "Log Out" }
let displayAlert = state
.filter { $0 == .check }
.map(to: ())
return Output(
buttonTitle: buttonTitle,
displayAlert: displayAlert
)
}
}
class ViewController: UIViewController {
var button: UIButton!
var viewModoel: ViewModel!
let disposeBag = DisposeBag()
func bind() {
let logout = PublishSubject<Void>()
let input = ViewModel.Input(
logButton: button.rx.tap.asObservable(),
okayButton: logout
)
let output = viewModoel.transform(input)
output.buttonTitle
.bind(to: button.rx.title(for: .normal))
.disposed(by: disposeBag)
output.displayAlert
.flatMap { [unowned self] _ in
self.areYouSure()
}
.bind(to: logout)
.disposed(by: disposeBag)
}
func areYouSure() -> Observable<Void> {
Observable.create { [unowned self] observer in
let alert = UIAlertController(
title: "Are you sure to sign out?",
message: nil,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(
title: "Yes", style: .destructive, handler: { _ in
observer.onNext(())
observer.onCompleted()
}
))
alert.addAction(UIAlertAction(
title: "No", style: .default, handler: { _ in
observer.onCompleted()
}
))
self.present(alert, animated: true)
return Disposables.create()
}
}
}

Related

Swift UIAlert doesn't waiting user response

func permissionInit() {
Task{
addListViewModel?.cameraPermission.accept(await chkCameraPermission())
addListViewModel?.photoLibraryPermission.accept(await chkPhotoLibraryPermission())
addListViewModel?.motionPermission.accept(await chkMotionPermission())
}
}
private func chkCameraPermission() async -> Bool{
let mediaType = AVMediaType.video
await AVCaptureDevice.requestAccess(for: mediaType)
let mediaAuthoriztionStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
switch mediaAuthoriztionStatus{
case .authorized:
print("ShopUp Camera Permission True")
return true
case .denied, .restricted, .notDetermined:
print("ShopUp Camera Permission False")
warningAlert(title: "권한 오류", infoMativeMsg: "")
return false
default:
return false
}
}
private func chkPhotoLibraryPermission() async -> Bool {
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
switch status {
case .authorized:
print("ShopUp Photo Permission True")
return true
case .denied, .restricted, .notDetermined:
print("ShopUp Photo Permission False")
warningAlert(title: "권한 오류", infoMativeMsg: "")
return false
default:
return false
}
}
private func chkMotionPermission() async -> Bool{
let coreMotionGranted = CMPedometer.authorizationStatus()
switch coreMotionGranted {
case .authorized:
print("ShopUp CoreMotion Permission True")
return true
case .notDetermined, .restricted, .denied :
print("ShopUp CoreMotion Permission False")
warningAlert(title: "권한 오류", infoMativeMsg: "")
return false
default:
return false
}
}
func warningAlert(title: String, infoMativeMsg: String, completionHandler: Void? = nil) {
let alert = UIAlertController(title: title, message: infoMativeMsg, preferredStyle: .alert)
if completionHandler != nil {
let okAction = UIAlertAction(title: "확인", style: .default, handler: {_ in completionHandler})
alert.addAction(okAction)
}else {
let okAction = UIAlertAction(title: "확인", style: .default)
alert.addAction(okAction)
}
self.present(alert, animated: true, completion: completionHandler != nil ? {completionHandler!} : nil)
}
I added UIAlert in ViewController but it doesn't wait user response and showing error.
I also tried await on self.present but not working too.
permissionInit has an await but it doesn't seem to work.
2023-01-09 14:45:37.015435+0900 ShopUp[544:94537] [Presentation] Attempt to present <UIAlertController: 0x12c03e000> on <UINavigationController: 0x12d019c00> (from <ShopUp.AddListViewController: 0x12ce08350>) while a presentation is in progress.
2023-01-09 14:45:37.015644+0900 ShopUp[544:94537] [Presentation] Attempt to present <UIAlertController: 0x12d07b800> on <UINavigationController: 0x12d019c00> (from <ShopUp.AddListViewController: 0x12ce08350>) while a presentation is in progress.
I would like to show UIAlert in oder.
I would be grateful if you could let me know which part is wrong.
You are calling three functions, all of which try to present an alert at the same time (or nearly so.) Only the first one succeeds because a view controller can only present a single other view controller. The other two fail which is why you get the two error messages.
Here is one way to concatenate the three requests so that each one will wait until the previous ones are complete:
extension UIViewController {
func permissionInit() {
let avCapture = Observable.createAsync { await AVCaptureDevice.requestAccess(for: .video) }
.filter { !$0 }
.observe(on: MainScheduler.instance)
.flatMap { [weak self] _ in self?.warningAlert(title: "권한 오류", infoMativeMsg: "") ?? Observable.empty() }
let phPhoto = Observable.createAsync { await PHPhotoLibrary.requestAuthorization(for: .readWrite) }
.filter { $0 != .authorized }
.observe(on: MainScheduler.instance)
.flatMap { [weak self] _ in self?.warningAlert(title: "권한 오류", infoMativeMsg: "") ?? Observable.empty() }
let cmPedo = Observable.just(CMPedometer.authorizationStatus())
.filter { $0 != .authorized }
.flatMap { [weak self] _ in self?.warningAlert(title: "권한 오류", infoMativeMsg: "") ?? Observable.empty() }
_ = Observable.concat(avCapture, phPhoto, cmPedo)
.subscribe(onNext: {
print("all requests complete.")
})
}
func warningAlert(title: String, infoMativeMsg: String) -> Observable<Void> {
Observable.deferred {
let result = PublishSubject<Void>()
let alert = UIAlertController(title: title, message: infoMativeMsg, preferredStyle: .alert)
let okAction = UIAlertAction(title: "확인", style: .default, handler: { _ in result.onSuccess(()) })
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
return result
}
}
}
extension Observable {
static func createAsync(_ asyncFunc: #escaping () async throws -> Element) -> Observable<Element> {
Observable.create { observer in
let task = Task {
do {
observer.onSuccess(try await asyncFunc())
} catch {
observer.onError(error)
}
}
return Disposables.create { task.cancel() }
}
}
}
public extension ObserverType {
func onSuccess(_ element: Element) -> Void {
onNext(element)
onCompleted()
}
}
The key is in using the concat operator which will only subscribe to one Observable at a time. It waits until an Observable stops before subscribing to the next one.
Learn more in this article: Recipes for Combining Observables in RxSwift
Lastly, I also agree with HangarRash.

After refactoring function inside closure is unused

I was extracting closure to method but I always get this error:
Function is unused
This is whole working func:
fileprivate func attemptToChangePassword() {
passwordChanger.change(securityToken: securityToken, oldPassword: oldPassword.text ?? "", newPassword: newPassword.text ?? "", onSuccess:{[weak self] in
self?.hideSpinner()
let alertController = UIAlertController(
title: nil,
message: "Your password has been successfully changed.",
preferredStyle: .alert)
let okButton = UIAlertAction(
title: "OK",
style: .default) { [weak self] _ in
self?.dismiss(animated: true)
}
alertController.addAction(okButton)
alertController.preferredAction = okButton
self?.present(alertController, animated: true)
}, onFailure: {[weak self] message in
self?.hideSpinner()
self?.showAlert(message: message) { [weak self] _ in
self?.oldPassword.text = ""
self?.newPassword.text = ""
self?.confirmPassword.text = ""
self?.oldPassword.becomeFirstResponder()
self?.view.backgroundColor = .white
self?.blurView.removeFromSuperview()
self?.cancel.isEnabled = true
}
})
}
This is how I extracted last closure to method:
fileprivate func startOver() -> (UIAlertAction) -> Void {
return { [weak self] _ in
self?.oldPassword.text = ""
self?.newPassword.text = ""
self?.confirmPassword.text = ""
self?.oldPassword.becomeFirstResponder()
self?.view.backgroundColor = .white
self?.blurView.removeFromSuperview()
self?.cancel.isEnabled = true
}
}
If I try this, the error "Function is unused" shows up:
onFailure: { [weak self] message in
self?.hideSpinner()
self?.showAlert(message: message) { [weak self] _ in
self?.startOver()//FUNCTION IS UNUSED
}
})
Edit:
Here is alert method that is used:
fileprivate func showAlert( message: String, okAction: #escaping (UIAlertAction) -> Void) {
let ac = UIAlertController(title: nil, message: message, preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler:okAction)
ac.addAction(ok)
ac.preferredAction = ok
self.present(ac, animated: true)
}
If I add back alert action button it works:
let okButton = UIAlertAction(
title: "OK",
style: .default,
handler:startOver())
You're currently calling that function inside of the action you pass to your showAlert function but then throwing away the action it returns. Instead, you want to pass the action that it returns directly to your showAlert method rather than wrapping it inside another action with the trailing closure syntax:
self?.showAlert(message: message, okAction: self!.startOver())

Can't pass data between View Controller and Model

There is UIAlertController with text field in my View Controller. When user enter name of the city, this data must be transmitted to Model, when I get coordinates of this city. But I can't to pass name of the city from View Controller to Model
My UIAlertController:
class MainScrenenViewController: UIViewController {
var delegate: ILocationGroup?
#objc func locationButtonTap() {
let alert = UIAlertController(title: "Add city", message: nil, preferredStyle: .alert)
let addButton = UIAlertAction(title: "Add", style: .default) { action in
self.delegate?.addLocation(alert.textFields?.first?.text ?? "No City")
}
alert.addAction(addButton)
let cancelButton = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alert.addAction(cancelButton)
alert.addTextField { textField in
textField.placeholder = "Your City"
}
present(alert, animated: true, completion: nil)
}
My Model:
protocol ILocationGroup {
func addLocation(_ name: String)
}
class LocationGroup: ILocationGroup {
var mainScreenViewController: MainScrenenViewController?
func addLocation(_ name: String) {
mainScreenViewController?.delegate = self
let url = "https://geocode-maps.yandex.ru/1.x/?apikey=fd93783b-fe25-4428-8c3b-38b155941c8c&format=json&geocode=\(name)"
guard let url = URL(string: url) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
do {
let result = try JSONDecoder().decode(LocationData.self, from: data)
print(result.response.geoObjectCollection.metaDataProperty.geocoderResponseMetaData.boundedBy.envelope.lowerCorner)
}
catch {
print("failed to convert \(error)")
}
}
task.resume()
}
}
I think it is supposed to be var delegate: LocationGroup()
Also, I wouldn't be calling it delegate because registered delegate is a keyword in swift
https://manasaprema04.medium.com/different-ways-to-pass-data-between-viewcontrollers-views-8b7095e9b1bf

makeAlert Function Returns Always False

The function below always returns false. I tried to put return inside of the completion it did not accept either.
Can you please help me?
// MARK: - make Alert for user Input
func makeAlert(message: String, defaultButtonText: String, cancelButtonText: String) - > Bool {
var answer = Bool()
let alert = UIAlertController(title: "Warning", message: message, preferredStyle: .alert)
let actionYes = UIAlertAction(title: defaultButtonText, style: .default) {
(action) in
answer = true
}
let actionNo = UIAlertAction(title: cancelButtonText, style: .default) {
(action) in
answer = false
}
alert.addAction(actionNo)
alert.addAction(actionYes)
self.present(alert, animated: true, completion: {
print(answer)
})
return answer
}
You have to use completion like this.
func makeAlert(message: String,defaultButtonText: String, cancelButtonText: String, completion: #escaping ((Bool) -> Void)) {
let alert = UIAlertController(title: "Warning", message: message, preferredStyle: .alert)
let actionYes = UIAlertAction(title: defaultButtonText, style: .default) { (action) in
completion(true)
}
let actionNo = UIAlertAction(title: cancelButtonText, style: .default) { (action) in
completion(false)
}
alert.addAction(actionNo)
alert.addAction(actionYes)
self.present(alert, animated: true, completion: {
})
}
Usage :
makeAlert(message: "Test", defaultButtonText: "Test", cancelButtonText: "Test") { (action) in
if action {
// Do code for true part
} else {
// Do code for false part
}
}
EDIT
As per the commnet. How to use in FSCalendar
func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool {
makeAlert(message: "Test", defaultButtonText: "Yeah", cancelButtonText: "No") { (action) in
if action {
calendar.select(date)
}
}
return false
}

How to call a function in a different class in swift

I have a MainCoordinator class extending NSObject and having the following methods:
init(presenter: UINavigationController) {
self.presenter = presenter
}
​func start() {
let mainViewController = MainViewController(userDefaults: UserDefaults.standard)
presenter.pushViewController(mainViewController, animated: false)
self.mainViewController = mainViewController
subscribeToEvents()
}
private func subscribeToEvents() {
if let viewModel = mainViewController?.viewModel {
viewModel.showOptions.subscribe(onNext: { [weak self] in
self?.showOptions()
}).disposed(by: disposeBag)
}
}
private func showOptions() {
let actionSheet = UIAlertController(title: "Would you like to open the Camera or select one from your photo library?",
message: nil,
preferredStyle: .actionSheet)
let cameraAction = UIAlertAction(title: "Use the Camera", style: .default) { [weak self] (_) in
self?.openCameraScanner()
}
let photoLibraryAction = UIAlertAction(title: "Open Photo Library", style: .default) { [weak self] (_) in
self?.openPhotoLibrary()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
actionSheet.addAction(cameraAction)
actionSheet.addAction(photoLibraryAction)
actionSheet.addAction(cancelAction)
mainViewController?.present(actionSheet, animated: true, completion: nil)
}
}
MainViewController is a class extending UIViewController which has the following button:
lazy private var sButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle('options', for: .normal)
button.backgroundColor = UIColor.yellow
button.roundedCorners(radius: 25)
button
.rx
.tap
.bind(to: self.viewModel.showOptions)
.disposed(by: self.disposableBag)
return button
}()
This is its init method
init(userDefaults: UserDefaults) {
viewModel = MainViewModel(userDefaults: userDefaults)
super.init(nibName: nil, bundle: nil)
}
The method self.viewModel.showOptions is of type PublishSubject, whenever I tap the sButton I want the showOptions method in MainCoordinator class to be activated.
import RxSwift
class MainViewModel {
let disposeBag = DisposeBag()
//Subjects
let showScanOptions = PublishSubject<Void>()
..
init(networkable: Networkable, userDefaults: UserDefaults) {
self.networkable = networkable
self.userDefaults = userDefaults
loginResponse.value = userDefaults.loginResponse
login.subscribe(onNext: { [weak self] in
guard let username = self?.email.value, let password = self?.password.value else { return }
self?.isLoading.onNext(true)
let loginInputs = LoginInputs(email: username, password: password)
self?.networkable.login(loginInputs: loginInputs) { loginResponse in
guard let loginResponse = loginResponse else {
self?.errorSubject.onNext(nil)
self?.isLoading.onNext(true)
return
}
self?.userDefaults.loginResponse = loginResponse
self?.loginResponse.value = loginResponse
}
}, onError: { [weak self] error in
self?.isLoading.onNext(false)
self?.errorSubject.onNext(error)
}).disposed(by: disposeBag)
}
}
How is this possible to achieve??
Any help is appreciated.