In Swift, it appears we infer types within the class however outside of functions. I understand that if a variable is only declared within a function then it will only live within that given scope. Isn't it best practice to instantiate objects outside of functions so that we can reference the same object as we program a viewController while also avoiding the possibility of crashes? And if not, then what is the purpose of inferring variables at the top of viewControllers and then instantiating the object within a function?
Here is my example code I'm following from a tutorial. Notice how mapView is inferred at the top of the viewController but instantiated in the loadView method. Wouldn't this make the mapView object only accessible to the loadView function but not to other methods:
import Foundation
import UIKit
import MapKit
import CoreLocation
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
var mapView: MKMapView!
var problemChild: Int!
override func viewWillDisappear(_ animated: Bool) {
print("the view disappeared")
}
override func loadView() {
mapView = MKMapView()
view = mapView
mapView.delegate = self
mapView.isPitchEnabled = true
// let atlLongLat = MKCoordinateRegion.init(center: CLLocationCoordinate2D.init(latitude: CLLocationDegrees.init(33.7490), longitude: CLLocationDegrees.init(84.3880)), span: MKCoordinateSpan.init(latitudeDelta: 33.7490, longitudeDelta: 84.3880))
//mapView.setRegion(atlLongLat, animated: true)
mapView.showsPointsOfInterest = true
mapView.showsBuildings = true
mapView.showsCompass = true
mapView.showsTraffic = true
let locationManager = CLLocationManager()
locationManager.delegate = self
let locationAuthStatus = CLLocationManager.authorizationStatus()
if locationAuthStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
mapView.showsUserLocation = true
let segmentedControl = UISegmentedControl.init(items: ["Standard", "Hybrid", "Satellite"])
segmentedControl.selectedSegmentIndex = 0
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
segmentedControl.backgroundColor = UIColor.yellow
view.addSubview(segmentedControl)
let zoomButtonFrame = CGRect.init(x: 0, y: 0, width: view.bounds.width, height: 400)
let zoomButton = UIButton.init(frame: zoomButtonFrame)
zoomButton.backgroundColor = UIColor.green
zoomButton.setTitle("Where Am I?", for: .normal)
zoomButton.setTitleColor(UIColor.black, for: .normal)
zoomButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(zoomButton)
let guide = view.safeAreaLayoutGuide
let topConstraint = segmentedControl.topAnchor.constraint(equalTo: guide.topAnchor, constant: 8)
let zoomButtonTopConstraint = zoomButton.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 559)
let margins = view.layoutMarginsGuide
let zoomButtonLeadingConstraint = zoomButton.leadingAnchor.constraint(equalTo: margins.leadingAnchor)
let leadingConstraint = segmentedControl.leadingAnchor.constraint(equalTo: margins.leadingAnchor)
let trailingConstraint = segmentedControl.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
let zoomButtonTrailingConstraint = zoomButton.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
topConstraint.isActive = true
leadingConstraint.isActive = true
trailingConstraint.isActive = true
zoomButtonTopConstraint.isActive = true
zoomButtonLeadingConstraint.isActive = true
zoomButtonTrailingConstraint.isActive = true
segmentedControl.addTarget(self, action:#selector(mapTypeChanged(segControl:)), for: .valueChanged)
zoomButton.addTarget(self, action: #selector(zoomButtonTapped(zoomButt:)), for: .touchUpInside)
}
#objc func mapTypeChanged(segControl: UISegmentedControl) {
switch segControl.selectedSegmentIndex {
case 0:
mapView.mapType = .standard
case 1:
mapView.mapType = .mutedStandard
case 2:
mapView.mapType = .satelliteFlyover
default:
break
}
}
#objc func zoomButtonTapped(zoomButt: UIButton){
let b: Int = problemChild
print(b)
for _ in 1...5 {
print("Pinging Your Location...")
if zoomButt.backgroundColor == UIColor.green{
print("this button's background color is green man.")
}
}
}
func mapViewWillStartLocatingUser(_ mapView: MKMapView) {
//adding this here to get used to the idea of protocols
}
}
Thank you in advance and I apologize for sounding like a noob but I'd really like to understand.
The scope of a variable is set by its definition, not its assignment. mapView is a property of MapViewController. Therefore it is accessible everywhere in MapViewController. That's unrelated to when it is assigned.
View controllers are a bit unusual because they are often initialized from storyboards, but some pieces cannot be initialized until viewDidLoad (because they reference pieces from the storyboard). That said, this is not the best code. It would have been better written this way:
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
let mapView = MKMapView()
...
And the line mapView = MKMapView() should be removed from loadView. The way it's written works, but it's not as clear or as safe as it should be. (loadView is only called one time in a view controller's life cycle. This wasn't always true, but has been true for longer than Swift has been around.)
When I say that this is not "as safe as it should be," I mean that if something were to access mapView between init and loadView (which can happen for various reasons), this would crash. By carefully managing things, you can avoid that, but it's safer just to avoid ! types when you can.
Variables declared outside of functions the way mapView is are instance variables. The scope of the variable is all the code that's available to the instance, so it's acceptable to reference the object from other functions.
By initializing it inside loadView, the object reference is only valid after that assignment executes but that's different from visibility of the variable.
You asked:
Isn't it best practice to instantiate objects outside of functions so that we can reference the same object as we program a viewController ... ?
If you’re asking the question of “don’t we favor properties over local variables”, the answer is “no”. In defensive programming, we favor local variables except in those cases where we actually need view controller level scope, in which case we use a property. Local variables are free of unintended data sharing/mutation. Properties, because they are shared amongst all the methods, entail a (admittedly modest) risk that it might be accidentally changed elsewhere. Now, where you need to reference the object in a variety of methods, such as the case with mapView, then a property is what you need. But if we don’t need to reference it elsewhere, such as the locationManager, then we stick with local variables if we can.
And if not, then what is the purpose of inferring variables at the top of viewControllers and then instantiating the object within a function?
First, we’re not “inferring variables” at the top of the view controller. We’re simply “declaring properties”, declaring that this will accessible throughout the view controller, regardless of where it is ultimately instantiated.
Regarding the practice of declaring a property up front, but only later instantiating the object and setting the property later within a function like viewDidLoad (or in your example, in loadView), this is not an unreasonable practice. It keeps the instantiation and configuration all together in one place.
If this really bothered you, then yes, you could go ahead and instantiate the object up where you declare the property. But if you were doing that, I might move the configuration of that object there, too, using a closure to both instantiate and configure the object:
class MapViewController: UIViewController {
lazy var mapView: MKMapView = {
let mapView = MKMapView()
mapView.delegate = self
mapView.isPitchEnabled = true
mapView.showsPointsOfInterest = true
mapView.showsBuildings = true
mapView.showsCompass = true
mapView.showsTraffic = true
mapView.showsUserLocation = true
return mapView
}()
...
override func loadView() {
view = mapView
if CLLocationManager.authorizationStatus() == .notDetermined {
CLLocationManager().requestWhenInUseAuthorization()
}
...
}
}
The only trick is that because the closure initializing mapView refers to self, we need to instantiate it lazily.
All of that having been said, I’m not concerned as others about the practice of just declaring the property up front, but instantiating/configuring it later in viewDidLoad/loadView. In fact, this is very common. In storyboards, for example, we decouple the process into three phases:
we declare of the property in the view controller and hook up the outlet in Interface Builder;
when the storyboard scene is instantiated, the storyboard instantiates the map view for us; and
we’ll do any additional programmatic configuration of the map view in viewDidLoad.
You said:
Here is my example code I'm following from a tutorial. Notice how mapView is inferred at the top of the view controller but instantiated in the loadView method. Wouldn't this make the mapView object only accessible to the loadView function but not to other methods:
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
var mapView: MKMapView!
...
override func loadView() {
mapView = MKMapView()
view = mapView
...
let locationManager = CLLocationManager()
...
}
}
No, just because we instantiated mapView in loadView does not limit its scope to that method. In fact, because we declared it as a property earlier, the opposite is true. It is accessible throughout the view controller methods. Do not conflate the declaration of a variable/property with its instantiation.
In this example, locationManager is a local variable (because it’s only used locally within this method), but mapView is a property of the view controller class. The mapView is instantiated and configured in loadView, but we make it a property of the view controller because the mapTypeChanged method needs access to it.
By the way, this technique of programmatically setting the root view of the view controller in loadView is a very uncommon practice nowadays. That is only for programmatically create views. Instead, often we use a storyboard (or in rare cases, a NIB/XIB), add the map view there, hook up the #IBOutlet in Interface Builder, and then viewDidLoad (not to be confused with loadView) could reference that outlet property:
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet var mapView: MKMapView!
...
override func viewDidLoad() {
super.viewDidLoad()
// perhaps reference the map view created in the storyboard to configure it, e.g.
mapView.isPitchEnabled = true
...
let locationManager = CLLocationManager()
...
}
}
For more information, see Displaying and Managing Views with a View Controller. Also see the View Controller Programming Guide.
I'm currently experimenting with MapKit and SpriteKit, however I have run into a problem.
When I try to convert map coordinates to a point on my screen (in my GameScene), I get an error: Unexpectedly found nil while unwrapping an optional value
The following code produces the error and is in a function that (for testing purposes) runs when the screen is tapped.
monster.position = (mapView?.convert(spawnLocation, toPointTo: view))!
monster is an SKSpriteNode declared at the top of my GameScene. spawnLocation is just a set of coordinates (I checked and they should be visible on the screen). mapView is declared at the top of my GameScene like so: var mapView = GameViewController().map (I believe my problem is here)
Checking if mapView contains a value or not results in nothing being printed to the console:
if (mapView != nil) {
print("not nil")
monster.position = (mapView?.convert(spawnLocation, toPointTo: view))!
}
My map shows up when I run the app, however nothing happens when the above code is executed. I think my problem is with the way I am currently accessing the map from the GameViewController, but I'm not sure how to fix it.
To clarify: my map is declared in my GameViewController and I need to access it from my GameScene so that I can convert the coordinates to a point in the view.
NOTE: I may just be using the convert function wrong.
GameViewController:
#IBOutlet var parentView: UIView!
#IBOutlet var map: MKMapView!
#IBOutlet var skview: SKView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
map.delegate = self
map.showsScale = false
map.showsPointsOfInterest = false
map.showsUserLocation = true
map.showsBuildings = true
map.isZoomEnabled = false
map.isScrollEnabled = false
map.isPitchEnabled = false
map.isRotateEnabled = false
locationManager.requestWhenInUseAuthorization()
if (CLLocationManager.locationServicesEnabled()) {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
let region = MKCoordinateRegionMakeWithDistance((locationManager.location?.coordinate)!, 400, 400)
map.setRegion(region, animated: false)
parentView.addSubview(map)
parentView.addSubview(skview)
GIDSignIn.sharedInstance().uiDelegate = self
}
So after continuing to work on my issue, I took another look at this question (I had already tried the solution in the answer - it didn't work).
I noticed that my problem might be that the scene in which I was trying to access the MKMapView/GameViewController from wasn't the first scene.
When I added the following to my code, it printed nil:
print("view controller", self.gameViewController)
I then looked at the code responsible for transitioning from the first scene to the one I was having issues with, and I added a line:
let signedInTransition = SKTransition.fade(withDuration: 1)
let nextScene = GameScene(size: scene!.size)
nextScene.scaleMode = .aspectFill
nextScene.gameViewController = self.gameViewController /* ADDED THIS LINE */
scene?.view?.presentScene(nextScene, transition: signedInTransition)
Turns out my problem was that my scene didn't actually have a GameViewController and therefore
let gameViewController:GameViewController!
never actually contained a value.
I'm working on an iOS app and there's one small bug. My project contains the following:
Custom searchbar on top of a mapView (HomeViewController.swift)
View with a tableview which displays the searchresults (LocationSearchTable.swift)
When searching I add the view with the tableview on top of my mapview and behind my searchbar. This all works fine.
But now when I select a searchresult from the tableView it crashes because it says that my mapView is nil.
I'm having the following in HomeViewController.swift:
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
mapView.delegate = self
}
Adding the LocationSearchTable to the mapView like this:
func didChangeSearchText(searchText: String) {
if (searchText == "" ) {
self.locationSearchTable.view.removeFromSuperview()
} else {
self.view.insertSubview(self.locationSearchTable.view, aboveSubview: mapView)
}
}
And on the LocationSearchTable.swift I want to call the mapView when tapping on something in the list:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let mapView = HomeViewController().mapView
print(mapView)
}
When trying to access the mapView from there it's nil. Why is that? And what am I missing here?
Kind regards,
Wouter
You creating a new instance of HomeViewController and then asking for its mapView before the method awakeFromNib has occurred. The mapView at that point will be nil until the HomeViewController controller is added to a screen
The best way to handle this if I'm reading your hierarchy right is to delegate back to the view with the map from you tableview.
Or you can pass a reference to mapView into The tableView
I'm making an a game in XCode 7 using Swift 2. I have a variable that I want to pass from the start screen (which is an UIViewController) to the game scene (which is an SKScene). I want the player to select a character in a UIView and play with it in the SKScene. I also want the score that's in the SKScene to show in the game-over screen that's an UIView. I've seen tutorials for passing data between two UIViewControllers and between two SKScenes, but none of them work for this case.
How can I pass a variable from an UIViewController to a SKScene (and vice versa)?
I ran over this same problem yesterday.
Swift 5.2, xcode 12 and targeting iOS 14. Searched high and low. Eventually found the userData attribute of the SKScene.
Note: The app is based on SwiftUI and the SKScene is inside a UIViewRepresentable:
struct SpriteKitContainer: UIViewRepresentable {
typealias UIViewType = SKView
var skScene: SKScene!
#Binding var timerData : ModelProgressTimer
Not that I am passing a binding into the View - this contains a number of data items I wanted to make available to the scene.
I placed code in two places to populate skScene.userData:
func makeUIView(context: Context) -> SKView {
self.skScene.scaleMode = .resizeFill
self.skScene.backgroundColor = .green
debugPrint("setting user data")
self.skScene.userData = [:]
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["percent"] = timerData.percentComplete
let view = SKView(frame: .zero)
view.preferredFramesPerSecond = 60
// view.showsFPS = true
// view.showsNodeCount = true
view.backgroundColor = .clear
view.allowsTransparency = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
self.skScene.userData = [:]
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["percent"] = timerData.percentComplete
view.presentScene(context.coordinator.scene)
}
Then, inside the skScene object, I am able to retrieve the userData values:
var item: SKShapeNode! = nil
override func sceneDidLoad() {
let scaleUp = SKAction.scale(by: 2.0, duration: 1.0)
let scaleDown = SKAction.scale(by: 0.5, duration: 1.0)
scaleUp.timingMode = .easeInEaseOut
scaleDown.timingMode = .easeInEaseOut
let sequence = SKAction.sequence([scaleUp,scaleDown])
actionRepeat = SKAction.repeatForever(sequence)
item = SKShapeNode(circleOfRadius: radius)
addChild(item)
}
override func didChangeSize(_ oldSize: CGSize) {
item.fillColor = self.userData!["color"] as! UIColor
item.strokeColor = .clear
let meRunning = self.userData!["running"] as! Bool
if meRunning {
item.run(actionRepeat)
} else {
item.removeAllActions()
}
}
This draws a circle that "pulses" if the "running" boolean is set in the userdata (a Bool value).
If you haven't already found the answer, I did find a way to do this. I was doing something very similar.
In my case I have a UIView that gets called as a pause menu from my scene. I have made the class and variable names a little more ambiguous so they can apply to your situation as well.
class MyView: UIView {
var scene: MyScene!
var button: UIButton!
init(frame: CGRect, scene: MyScene){
super.init(frame: frame)
self.scene = scene
//initialize the button
button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "buttonTapped"))
}
func buttonTapped(){
self.removeFromSuperview() // this removes my menu from the scene
scene.doTheThing() // this calls the method on my scene
}
Here are the relevant parts of my scene.
class MyScene: SKScene {
func doTheThing(){
//this is a function on the scene. You can pass any variable you want through the function.
}
}
In your situation, it sounds like the first screen is a UIView and the second screen is the SKScene.
You may want to make your SKScene first, pause it, and then add the UIView in front of the Scene. Once the character is selected, you can remove the UIView and add your character into the scene.
I hope that this answers your question. If it doesn't let me know.
From the level select scene:
let scene = GameScene(fileNamed:"GameScene")
scene?.userData = [:]
scene?.userData!["level"] = 21
self.view?.presentScene(scene!)
From within the game scene:
let level = self.userData!["level"] as! Int
I've currently developing an iOS application with a dark theme, and would really like to make the displayed Apple Map also in dark colors.
I've Googled the topic and tried to change the properties of the component through the Storyboard. But, I couldn't find anything.
Is this even possible? Can I change the colors of the Apple Map component... or even just invert the color to make it look dark?
Thank you.
It's a little clumsy and limited, but you could put a UIView with black background color and alpha less than 1.0 (e.g. 0.3) over the map (make sure to turn off user interaction with this view so gestures get passed through to the map) and that would dim the whole thing. A little kludgy and you lose contrast, but it might be worth trying.
You can't do this without using third-party providers like MapBox, at least on iOS 6 and below.
You can create a subclass of MKTileOverlay like
class DarkModeMapOverlay: MKTileOverlay {
init() {
super.init(urlTemplate: nil)
canReplaceMapContent = true
}
override func url(forTilePath path: MKTileOverlayPath) -> URL {
let tileUrl = "https://a.basemaps.cartocdn.com/dark_all/\(path.z)/\(path.x)/\(path.y).png"
return URL(string: tileUrl)!
}
}
and then set the overlay on your MKMapView with
class MapViewController: UIViewController, MKMapViewDelegate {
private var tileRenderer: MKTileOverlayRenderer?
private var mapView: MKMapView {
return view as! MKMapView
}
private func configureTileRenderer() {
let darkModeOverlay = DarkModeMapOverlay()
tileRenderer = MKTileOverlayRenderer(tileOverlay: darkModeOverlay)
mapView.addOverlay(darkModeOverlay, level: .aboveLabels)
}
override func loadView() {
view = MKMapView(frame: .zero)
}
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
configureTileRenderer()
}
// MARK: MKMapViewDelegate
func mapView(_: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
guard let renderer = self.tileRenderer else {
return MKOverlayRenderer(overlay: overlay)
}
return renderer
}
}