iOS app crashes when tries to add child viewcontroller with mapView inside - swift

I have tabbarController where i put parent viewController with container view inside.
public override func viewDidLoad() {
viewControllers = [
ParentViewController()
]
}
On init i'm initializing 2 child view controllers and adding 1st controller (that does't contain MapView) as child viewController.
At some point of time i need to switch between child controllers, and in that point app crashes
public class ParentViewController: UIViewController {
#IBOutlet weak var containerView: UIView!
let firstChildController: ViewControllerWithoutMapView
let secondChildController: ViewControllerWithMapView
init() {
firstChildController = ViewControllerWithoutMapView()
secondChildController = ViewControllerWithMapView()
super.init(nibName: "ParentViewController", bundle: nil)
}
public override func viewDidLoad() {
firstChildController.view.frame = containerView.bounds
addChildViewController(firstChildController)
firstChildController.willMoveToParentViewController(nil)
containerView.addSubview(firstChildController.view)
firstChildController.didMoveToParentViewController(self)
}
func switchChildControllers() {
secondChildController.view.frame = containerView.bounds <<<<< crash here
.....
}
}
I know about crashes that appears if you're not importing MapKit, i tried to import it everywhere - no luck.
What is the correct way to switch child viewControllers with MapView inside one of it?

Related

How to use instance of API from Main View in Container View?

If i create an instance of mapView in MainView, how can i use that instance in Container View?
class MainView: UIViewController {
var mapView = MapView()
}
class ContainerView: UIViewController {
MainView.mapView.changeCameraPosition()
}
How can i access this mapView instance in container View or is it possible?
The solution i found and also with help from Burnsi and Rob is to pass the instance as an object when adding view controller to container view:
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let nextPageVC = storyboard.instantiateViewController(withIdentifier:"NextPage") as! NextPage
nextPageVC.mapView = mapView
addChild(nextPageVC)
containerView.addSubview(nextPageVC.view)
nextPageVC.view.frame = containerView.bounds
nextPageVC.didMove(toParent: self)
And i also changed the class type of the Container View and Main View in NextPageVC to PassthruView so that users can interact with the map when Container View Controller is on top:
import Foundation
import UIKit
class PassthruView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Get the hit view we would normally get with a standard UIView
let hitView = super.hitTest(point, with: event)
// If the hit view was ourself (meaning no subview was touched),
// return nil instead. Otherwise, return hitView, which must be a subview.
return hitView == self ? nil : hitView
}
}
If Main View has the Container View, then the scenario would be like this if I'm not wrong:
class MainView: UIViewController {
var mapView = MapView()
var containerView = ContainerView()
}
If that's the case, you can make a method inside ContainerView class that takes an object of MapView as a parameter and call that method inside MainView class on containerView (the object of ContainerView that you have in MainView class).
The method could be something like this:
private func changeCameraPosition(mapView: MapView) {
mapView.changeCameraPosition()
}
And call this method on containerView object in Main View class as:
containerView.changeCameraPosition(mapView: mapView)

Xcode: didTransition to Won't Run in iMessage Extension

I am making an iMessage extension that uses the didTransition(to:). However, the function won't run when I resize the iMessage extension in the simulator. Am I doing something wrong?
This is the code I have:
import UIKit
import Messages
class EditorViewController: MSMessagesAppViewController {
#IBOutlet weak var input: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
input.text = "not changed"
}
// This part isn't working:
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
input.text = "changed"
}
}
When I resize it in the simulator, nothing happens. The input.text changes the UITextView's text in the viewDidLoad() function, but not in the didTransition(to) function because it never runs.
Am I doing something wrong?
The EditorViewController is a view controller presented by the show (e.g. Push) segue, and has a NavigationController attached to it.
Here is a gif of it not changing:
The input's text never changes
How can I fix this?
EDIT: The willTransition and didTransition functions don't run when the View Controller is embedded in a Navigation Controller. Is there a way to fix this? (It works without the Navigation Controller, but I need the Navigation Controller for this project).
As pointed out in this answer, the entry point of a iMessage App need to be a subclass of MSMessagesAppViewController, so you can not use a NavigationViewController directly as root controller, until Apple adds support for this behavior.
But as suggested, you could solve this with a workaround like this:
import UIKit
import Messages
class MyRootVC: MSMessagesAppViewController {
var navVC: UINavigationViewController!
var editorVC: EditorViewController!
func viewDidLoad() {
super.viewDidLoad()
editorVC = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! EditorViewController
navVC = UINavigationController(rootViewController: editorVC)
self.addChild(navVC)
self.view.addSubview(navVC.view)
navVC.didMove(toParent: self)
}
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
editorVC.input.text = "changed"
}
}
class EditorViewController: UIViewController {
#IBOutlet weak var input: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
input.text = "not changed"
}
}

SnapKit snp can't assign to UIHostingController

On many of projects I use SnapKit. And on new project too. On project I have ViewController which connected with SwiftUI view:
class OfficeListViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let vc = UIHostingController(rootView: OfficeListView())
addChild(vc)
view.addSubview(vc.view)
vc.didMove(toParent: self)
vc.view.translatesAutoresizingMaskIntoConstraints = false
// Here I want to set constraints to vc
vc.snp // throws error: Value of type 'UIHostingController<OfficeView>' has no member 'snp'
}
}
struct OfficeListView: View {
var body: some View {
Text("View")
}
}
But it throws error:
Value of type 'UIHostingController' has no member 'snp'
How to correctly use SnapKit with it?
The UIHostingController is just subclass of UIViewController and it renders SwiftUI view inside regular UIView. If you want to set up constrains, then you should use vc.view as we usually do with views.

views are merged instead of showing them separately

I have two xib file, one which shows login view and another which shows the steps what to do after the login is successful. I am having hard time to make it work. I have created macos project not ios and using safariservices so that it will work for the safari extension either.
Here is what i have done
import SafariServices
class SafariExtensionViewController: SFSafariExtensionViewController {
#IBOutlet weak var passwordMessage: NSTextField!
#IBOutlet weak var emailMessage: NSTextField!
#IBOutlet weak var message: NSTextField!
#IBOutlet weak var email: NSTextField!
#IBOutlet weak var password: NSSecureTextField!
static let shared = SafariExtensionViewController()
override func viewDidLoad() {
self.preferredContentSize = NSSize(width: 300, height: 250)
message.stringValue = ""
emailMessage.stringValue = ""
passwordMessage.stringValue = ""
}
override func viewDidAppear() {
if let storedEmail = UserDefaults.standard.object(forKey: "email") as? String {
if let stepView = Bundle.mainBundle.loadNibNamed(NSNib.Name(rawValue: "ExtensionStepsViewController"), owner: nil, topLevelObjects: nil)[0] {
self.view.addSubview(stepView)
}
}
}
#IBAction func userLogin(_ sender: Any) {
let providedEmailAddress = email.stringValue
let providedPassword = password.stringValue
let isEmailAddressValid = isValidEmailAddress(emailAddressString: providedEmailAddress)
self.message.stringValue = ""
emailMessage.stringValue = ""
passwordMessage.stringValue = ""
if isEmailAddressValid && providedPassword.count > 0 {
/* login process is handled here and store the email in local storage /*
/* TODO for now email is not stored in browser localstorage which has to be fixed */
let controller = "ExtensionStepsViewController"
let subview = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: controller), bundle: nil)
self.view.addSubview(subview.view)
}
}
}
This way i get error like Type Bool has no subscript members my file structure looks something like this.
SafariExtensionViewController.xib (main one which is shown initially
with login screen)
SafariExtensionViewController.swift
ExtensionStepsViewController.xib(this view should be shown when user
is logged in instead of login screen)
ExtensionStepsViewController.swift
I am using xcode 10, swift 4, everything new.
UPDATE
I used the following block both in viewDidAppear(if there is email in localstorage then show extension steps view instead of login screen) and inside login function when the login is success but it does not navigate to that ExtensionStepsView
let controller = "ExtensionStepsViewController"
let subview = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: controller), bundle: nil)
self.view.addSubview(subview.view)
Use case is show login at initial but if user is logged in then show another view but issue is now the view are merged
You got the error "Type Bool has no subscript members" because loadNibNamed(_:owner:topLevelObjects:) method of Bundle returns Bool struct that has no subscript members so you can't write like
true[0]
How to use this method correctly see the link and example from there:
var topLevelObjects : NSArray?
if Bundle.main.loadNibNamed(NSNib.Name(rawValue: "ExtensionStepsViewController"), owner: self, topLevelObjects: &topLevelObjects) {
let topLevelObjects!.first(where: { $0 is NSView }) as? NSView
}
Views were merged because you didn't remove previous views from the superview and added view from ExtensionStepsViewController to the same superview.
You can do the following steps to complete your issue:
Make SafariExtensionViewController inherited from SFSafariExtensionViewController that will be container (and parent) for two child view controllers such as LoginViewController and ExtensionStepsViewController and will be used to navigate between ones.
Make separately LoginViewController and ExtensionStepsViewController (both inherited from simple NSViewController) and its xibs.
Right after user logins transit from LoginViewController to ExtensionStepsViewController
As an example but instead of ParentViewController you have to use your implementation SafariExtensionViewController as I explain above in the first step.
public protocol LoginViewControllerDelegate: class {
func loginViewControllerDidLoginSuccessful(_ loginVC: LoginViewController)
}
public class LoginViewController: NSViewController {
weak var delegate: LoginViewControllerDelegate?
#IBAction func login(_ sender: Any) {
// login logic
let isLoginSuccessful = true
if isLoginSuccessful {
self.delegate?.loginViewControllerDidLoginSuccessful(self)
}
}
}
public class ExtensionStepsViewController: NSViewController {
}
public class ParentViewController: NSViewController, LoginViewControllerDelegate {
weak var login: LoginViewController! // using of force unwrap is anti-pattern. consider other solutions
weak var steps: ExtensionStepsViewController!
public override func viewDidLoad() {
let login = LoginViewController(nibName: NSNib.Name(rawValue: "LoginViewController"), bundle: nil)
login.delegate = self
// change login view frame if needed
login.view.frame = self.view.frame
self.view.addSubview(login.view)
// instead of setting login view frame you can add appropriate layout constraints
self.addChildViewController(login)
self.login = login
let steps = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: "ExtensionStepsViewController"), bundle: nil)
steps.view.frame = self.view.frame
self.addChildViewController(steps)
self.steps = steps
}
// MARK: - LoginViewControllerDelegate
public func loginViewControllerDidLoginSuccessful(_ loginVC: LoginViewController) {
self.transition(from: self.login, to: self.steps, options: .slideLeft) {
// completion handler logic
print("transition is done successfully")
}
}
}
Here is a swift playground with this example.
UPD:
You can instantiate NSViewController in several ways:
Use NSStoryboard that allows to load view of NSViewController from .storyboard file:
let storyboard = NSStoryboard(name: NSStoryboard.Name("NameOfStoryboard"), bundle: nil)
let viewController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("NSViewControllerIdentifierInStoryboard"))
Use appropriate initialiser of NSViewController to load view of it from .xib file:
let steps = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: "ExtensionStepsViewController"), bundle: nil)
Use default initialiser but you have to load view directly by overriding loadView() method if name of xib file is different from name of view controller class:
let steps = ExtensionStepsViewController()
// Also you have to override loadView() method of ExtensionStepsViewController.

Sending data to another view: can't unwrap option

I know that this has to be a simple fix, but can't seem to understand why my code is not working. Basically I am trying to send a value from a text field in 1 view to a 2nd view's label.
ViewController.swift
#IBOutlet var Text1st: UITextField
#IBAction func Goto2ndView(sender: AnyObject) {
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
//view2.Label2nd.text=text;
self.navigationController.pushViewController(view2, animated: true)
}
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = (VC.Text1st.text) //it doesn't like this, I get a 'Can't unwrap Option.. error'
println(string)
}
-------EDITED UPDATED CODE FROM (drewag)-------
ViewController.swift
let text = "text"
var sendString = Text1st.text
println(sendString) //successfully print it out.
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
view2.Label2nd.text=sendString;
self.navigationController.pushViewController(view2, animated: true)
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = self.Label2nd.text
println(string) //still getting the error of an unwrap optional.none
}
var VC = ViewController() creates a new instance of ViewController. Unless there is a default value, you are not going to get any value out of VC.Text1st.text. You really should use a string variable on your second view controller to pass the data to it.
Also, a note on common formatting:
Class names should start with a capital letter (as you have)
Method / function names should start with a lower case letter
UIViewController subclasses should have "Controller" included in their name, otherwise, it looks like it is a subclass of UIView which is an entirely different level of Model View Controller (the architecture of all UIKit and Cocoa frameworks)
Edit:
Here is some example code:
class ViewController1 : UIViewController {
...
func goToSecondView() {
var viewController = ViewController2()
viewController.myString = "Some String"
self.navigationController.pushViewController(viewController, animated: true)
}
}
class ViewController2 : UIViewController {
var myString : String?
func methodToUseMyString() {
if let string = self.myString {
println(string)
}
}
...
}
Note, I am not creating ViewController2 using a storyboard. I personally prefer avoiding storyboards because they don't scale well and I find editing them to be very cumbersome. You can of course change it to create the view controller out of the storyboard if you prefer.
jatoben is correct that you want to use optional binding. IBOutlets are automatically optionals so you should check the textfield to see if it is nil.
if let textField = VC.Text1st {
println(textField.text)
}
This should prevent your app from crashing, but it will not print out anything because your text field has not yet been initialized.
Edit:
If you want to have a reference to your initial ViewController inside your second you're going to have to change a few things. First add a property on your second viewcontroller that will be for the first view controller:
#IBOutlet var Label2nd: UILabel //existing code
var firstVC: ViewController? //new
Then after you create view2, set it's firstVC as the ViewController you are currently in:
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2 //already in your code
view2.firstVC = self //new
Finally in your viewDidLoad in your second view controller, use firstVC instead of the ViewController you recreated. It will look something like this:
override func viewDidLoad() {
super.viewDidLoad()
if let textField = firstVC?.Text2nd {
println(textField.text)
}
}
Use optional binding to unwrap the property:
if let string = VC.Text1st.text {
println(string)
}