I am currently getting to grips with UIKit and trying to build a simple game to do so. The first part I am struggling to understanding is auto layout, the necessary constraints and their behaviour, as well as view hierarchy.
Here is a basic UIViewController that is simply trying to place a game board (UIImageView) onto its root view.
class GameView {
var gameBoard: UIImageView = UIImageView(image: UIImage(named: "grid"))
}
class SinglePlayerGameViewController: UIViewController {
var gameModel: GameModel = GameModel()
var gameView: GameView = GameView()
override func viewDidLoad() {
super.viewDidLoad()
self.setUpGameBoard()
//self.startGame()
}
private func setUpGameBoard() -> Void {
print("screen size", UIScreen.main.bounds.size)
self.gameView.gameBoard.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.gameView.gameBoard)
self.gameView.gameBoard.contentMode = .scaleAspectFit
self.gameView.gameBoard.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
print("vc center X", self.view.center.x)
print("gb center X", self.gameView.gameBoard.center.x)
self.gameView.gameBoard.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
print("vc center Y", self.view.center.y)
print("gb center Y", self.gameView.gameBoard.center.y)
self.gameView.gameBoard.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9).isActive = true
print("vc width bounds", self.view.bounds.size.width)
print("vc width frame", self.view.frame.size.width)
print("gb width bounds", self.gameView.gameBoard.bounds.size.width)
print("gb width frame", self.gameView.gameBoard.bounds.size.width)
}
}
The output of the print statements below is as follows:
screen size (428.0, 926.0)
vc center X 214.0
gb center X 375.0
vc center Y 463.0
gb center Y 500.0
vc width bounds 428.0
vc width frame 428.0
gb width bounds 750.0
gb width frame 750.0
What confuses me is the following:
According to the quick help in Xcode, the centerXAnchor is defined as: A layout anchor representing the horizontal center of the view’s frame. So I am struggling to understand why the output for center x-coordinates for the parent view and the game board is different? The same thing goes for the center y-coordinate.
The widthAnchor is defined as: A layout anchor representing the width of the view’s frame.. So I would think that applying the multiplier of .9 would result in the width of my game board being exactly 90% of the parent view's frame, but that doesn't seem to be the case here and I can't really see why.
The funny thing is that the simulator is still displaying it properly, like so:
If there is anyone that could shed some light on this, I would very much appreciate it!
Thanks
Because you are doing your print statements in viewDidLoad which is before your autolayout constraints have taken effect. You can create constraints there, but they are not applied fully until layout time. Move all the print statements into viewDidLayoutSubviews, keeping everything else the same, and you will get the results you expect.
Related
I'm stuck at the Consolidation IV challenge from hackingwithswift.com.
Right now I'm trying to create a hangman game. I thought to place placeholder labels based on the length of the answer word. These placeholder labels would be placed inside a frame, which then would be placed in the center of the main view.
Unfortunately, the leading edge of the frame is placed centered. In my opinion, this is not a problem of constraints, but rather a problem of me creating the frame wrong.
My current code is
import UIKit
class ViewController: UIViewController {
var answer: String!
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - declare all the labels here
let letterView = UIView()
letterView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(letterView)
// MARK: - set constraints to all labels, buttons etc.
NSLayoutConstraint.activate([
letterView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
letterView.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor)
])
// MARK: - populate the letterView
// set the size of the placeholder
answer = "Atmosphäre"
let height = 60
let width = 25
// var width: Int
for placeholder in 0..<answer.count {
// create new label and give it a big font size
let placeholderLabel = UILabel()
placeholderLabel.text = "_"
placeholderLabel.font = UIFont.systemFont(ofSize: 36)
// calculate frame of this label
let frame = CGRect(x: placeholder * width, y: height, width: width, height: height)
placeholderLabel.frame = frame
// add label to the label view
letterView.addSubview(placeholderLabel)
}
}
}
The simulator screen looks just like this:
I already searched for answers on stackoverflow, but wasn't successful. I think I don't know what I'm exactly looking for.
The main problem, is that the letterView has no size, because no width or height constraints are applied to it.
To fix your code make the letterView big enough to contain the labels you've added as subviews by adding height and width constraints after the for loop:
for placeholder in 0..<answer.count {
...
}
NSLayoutConstraint.activate([
letterView.widthAnchor.constraint(equalToConstant: CGFloat(width * answer.count)),
letterView.heightAnchor.constraint(equalToConstant: CGFloat(height))
])
I'm not sure if you've covered this in your course yet, but a better way to go about this (which would take much less code), is to use a UIStackView as your letterView instead.
An extra thing to consider:
If you give the letterView a background color, you'll see that the labels are actually aligned outside of its bounds:
That's because you're setting each label's y position to be height, when it should probably be zero:
let frame = CGRect(x: placeholder * width, y: 0, width: width, height: height)
Correcting this places the labels within the bounds of the letterView:
Hi I'm just wanting to have a childView centered on the x axis of a NSViewController view using auto layout. I'm doing the following:
override func viewWillAppear()
{
super.viewWillAppear()
let childView = PagesView.init(frame: CGRect(x: 0, y: 0, width:100, height: 100))
childView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(childView)
childView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}
The childView doesn't even appear on the screen. When I remove the last line, then it gets positioned at 0,0. So something about that last line is causing it go haywire and I'm not sure why? I've used this exact same logic on iOS and it has worked fine.
All views should have constraints to define its location and size.
// This would create dimension constraints if your class cannot already infer them
childView.widthAnchor.constraint(equalToConstant: 100).isActive = true
childView.heightAnchor.constraint(equalToConstant: 100).isActive = true
You also need to specify a vertical anchor constraint so the view has enough information to be displayed.
childView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
Is there a way to make the corner radius of a UIView adept to the view it belongs to? I'm not comfortable with the idea of hard-coding corner radius values, because as soon as the width or the height of your view changes (on different screen orientations for example), the corners will look totally different. For example, take a look at WhatsApp's chat window.
As you can see, every message container view has a different width and a different height, but the curve of the corners are all exactly the same. This is what I'm trying to achieve. I want the curves of my corners to be the same on every view, no matter what the size of the view is or what screen the view is displayed on. I've tried setting the corner radius relative to the view's height (view.layer.cornerRadius = view.frame.size.height * 0.25) and I've also tried setting it to the view's width, but this doesn't work. The corners still look weird as soon as they are displayed on a different screen size. Please let me know if there's a certain formula or trick to make the curves look the same on every view/screen size.
Here's the best I can do. I don't know if this will be of help, but hopefully it will give you some ideas.
First the code:
class ViewController: UIViewController {
let cornerRadius:CGFloat = 10
let insetValue:CGFloat = 10
var numberOfViews:Int = 0
var myViews = [UIView]()
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
}
override func viewWillLayoutSubviews() {
setNumberOfViews()
createViews()
createViewHierarchy()
addConstraints()
}
func setNumberOfViews() {
var smallerDimension:CGFloat = 0
if view.frame.height < view.frame.width {
smallerDimension = view.frame.height
} else {
smallerDimension = view.frame.width
}
let viewCount = smallerDimension / (insetValue * 2)
numberOfViews = Int(viewCount)
}
func createViews() {
for i in 1...numberOfViews {
switch i % 5 {
case 0:
myViews.append(MyView(UIColor.black, cornerRadius))
case 1:
myViews.append(MyView(UIColor.blue, cornerRadius))
case 2:
myViews.append(MyView(UIColor.red, cornerRadius))
case 3:
myViews.append(MyView(UIColor.yellow, cornerRadius))
case 4:
myViews.append(MyView(UIColor.green, cornerRadius))
default:
break
}
}
}
func createViewHierarchy() {
view.addSubview(myViews[0])
for i in 1...myViews.count-1 {
myViews[i-1].addSubview(myViews[i])
}
}
func addConstraints() {
for view in myViews {
view.topAnchor.constraint(equalTo: (view.superview?.topAnchor)!, constant: insetValue).isActive = true
view.leadingAnchor.constraint(equalTo: (view.superview?.leadingAnchor)!, constant: insetValue).isActive = true
view.trailingAnchor.constraint(equalTo: (view.superview?.trailingAnchor)!, constant: -insetValue).isActive = true
view.bottomAnchor.constraint(equalTo: (view.superview?.bottomAnchor)!, constant: -insetValue).isActive = true
}
}
}
class MyView: UIView {
convenience init(_ backgroundColor:UIColor, _ cornerRadius:CGFloat) {
self.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = backgroundColor
self.layer.cornerRadius = cornerRadius
}
}
Explanation:
This is fairly simple code. The intent was to create as deeply nested a view hierarchy as possible, and, using auto layout, have two main variables: cornerRadius (the view's corner radius) and insetValue (the "frame's" inset). These two variables can be adjusted for experimenting.
The bulk of the logic is in viewWillLayoutSubviews, where the root view frame size is know. Since I'm using 5 different background colors, I'm calculating how many views can fit in the hierarchy. Then I'm creating them, followed by creating the view hierarchy, and finally I'm adding the constraints.
Experimenting and conclusions:
I was able to see what your concern is - yes, if a view's size components are smaller than the corner radius, you end up with inconsistent looking corners. But these values are pretty small - pretty much 10 or less. Most views are unusable at that size. (If I recall even the HIG suggests that a button should be no less than 40 points in size. Sure, even Apple breaks that rule. Still.)
If your 'insetValueis sufficiently larger than the corner radius, you should never have an issue. Likewise, using the iMessage scenario, a singleUILabelcontaining text and/or emoticons should have enough height that a noticeablecornerRadius` can be had.
The key point to set things like cornerRadius and insetValue is in viewWillLayoutSubviews, when you can decide (1) which is the smaller dimension, height or width, (2) how deeply you can nest views, and (3) how large of a corner radius you can set.
Use auto layout! Please note the absolute lack of frames. Other than determining the root view's dimensions at the appropriate time, you can write very compact code without worrying about device size or orientation.
I want to clip the bounds of my UIView perfectly to interact as a circle, but however I set the corner radius, mask and clip to bounds and it shows correctly, it moves as a square, as you can see in the image:
The code I have used is:
let bubble1 = UIView(frame: CGRectMake(location.x, location.y, 128, 128))
bubble1.backgroundColor = color2
bubble1.layer.cornerRadius = bubble1.frame.size.width/2
bubble1.clipsToBounds = true
bubble1.layer.masksToBounds = true
What is wrong there that does still keeping the edges of the view?
PD: All the views moves dynamically, so when it moves and hit each other, it shows these empty space, acting as an square instead of as an circle
Finally, after all I found what to implement, and was just that class instead of UIView:
class SphereView: UIView {
// iOS 9 specific
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .Ellipse
}
}
Seen here: https://objectcoder.com/2016/02/29/variation-on-dropit-demo-from-lecture-12-dynamic-animation-cs193p-stanford-university/
I am having deep trouble understanding how the resolution on different devices work. so what I have done is made a background image 1024 x 768 then tested on an iphone 5 device. when i run it, the bottom half of the screen is chopped off so as the top. I did use AspectFill as my scale mode, but even when i test it on iphone 6 or 6+, the background is still chopped off.
the following is my code for gameviewcontroller
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size: CGSize(width: 1024, height: 768))
let skView = self.view as! SKView
skView.multipleTouchEnabled = true
if GameSettings.Debugging.ALL_TellMeStatus {
skView.showsFPS = GameSettings.Debugging.ALL_ShowFrameRate
skView.showsNodeCount = GameSettings.Debugging.ALL_ShowNodeCount
skView.showsDrawCount = GameSettings.Debugging.IOS_ShowDrawCount
skView.showsQuadCount = GameSettings.Debugging.IOS_ShowQuadCount
skView.showsPhysics = GameSettings.Debugging.IOS_ShowPhysics
skView.showsFields = GameSettings.Debugging.IOS_ShowFields
}
skView.ignoresSiblingOrder = true
scene.scaleMode = .AspectFill
_ = SGResolution(screenSize: view.bounds.size, canvasSize: scene.size)
skView.presentScene(scene)
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return .Landscape
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
and my code for adding the background image in.
let background = SKSpriteNode(imageNamed: "Artboard 1")
background.posByCanvas(0.5, y: 0.5)
background.xScale = 1.2
background.yScale = 1.2
background.size = self.frame.size
background.zPosition = -1
addChild(background)
is this the right way to start the screen size or should i start with an iPad screen size then descale it? If anyone can lead me on how to attack this problem that would be great!!
Aspect fill is going to chop off parts of your background.
Your scene has an aspect ratio of 4:3
Your device has an aspect ratio of 16:9
AspectFill will scale your scene in its current aspect ratio(4:3) till the farthest borders are filled, allowing for the entire view to show the scene and have no black borders;
AspectFit will scale till the nearest borders are filled in, which will give you black bars to preserve the aspect ratio.
If you want the 4:3 to not be preserved, then you use .Fill, this of course is going to make your sprites fatter on 16:9 because it will just stretch the scene till it hits all 4 borders
Finally you have .ResizeFill, and what this does is resizes the scene bounds to match your screen size, so if you start with a 1024x768 scene, it will turn it into a 736x414 scene when you view it on iPhone 6+ (This is because we are working in points, not pixels, the pixel count would be x3 (so 2208x1242) which hardware will shrink to 1920x1080).
You need to take a step back and evaluate how you want your game to look on 3 different aspect ratios
16:9,3:2, and 4:3, and determine which of the 4 methods above works best for you.
Now in my opinion, I find that Aspect Fill works best, and I just plan for the 16:9 aspect ratio, making sure I do not put any important information in the areas that may be cropped for a game, but this does not work for everybody, and unfortunately nobody can help you with this part, since it is your game and you are the one that determines its look and feel.