前回のPart 1と同様に、簡単な画面を作成していきます。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let square = UIView()
square.backgroundColor = .red
view.addSubview(square)
// frame.sizeを指定し
square.frame.size = CGSize(width: 100, height: 100)
// 位置情報を指定することも出来る
square.center = view.center
// UIButtonクラスを使用してみる
let button = UIButton()
button.backgroundColor = .green
button.setTitle("Hello", for: .normal)
button.setTitleColor(.black, for: .normal)
// buttonにアクションを追加
button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
view.addSubview(button)
let buttonSize: CGFloat = 100
button.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: view.bounds.height*3/4 - buttonSize/2, width: buttonSize, height: buttonSize)
}
// "hello"とprintする簡単なfunction
// @objcキーワードは、Objective-CというSwiftの前身の言語のAPIを使う場合のもの。button.addTargetを使う場合は必要。
@objc private func onButtonTapped() {
print("hello")
}
}
buttonをクリックすると”hello”とprintされます。
さて、ここで、buttonをクリックすると赤い四角が黄色になる仕組みを組もうとするとどうすればいいでしょうか?
onButtonTapped( )の中にこう書きたいのですが、
@objc private func onButtonTapped() {
square.backgroundColor = .yellow
}
このままだと「Cannot find ‘square’ in scope」というエラーになります。’square’ という名前のオブジェクトがスコープ内に見つからない、という意味です。
さて、現時点ではsquareはviewDidLoad( )内で定義されています。関数の中で定義された変数(variable, varキーワードで定義)や定数(constant, letキーワードで定義は関数の中でしか扱えません。これを変数や定数の「スコープ」(範囲)といいます。
onButtonTapped( )内で扱えるようにするには、その関数と変数のスコープを合わせます。つまり、同じclass内のトップレベルで定義します。
class ViewController: UIViewController {
let square: UIView = {
let square = UIView()
square.backgroundColor = .red
return square
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(square)
square.frame.size = CGSize(width: 100, height: 100)
square.center = view.center
let button = UIButton()
button.backgroundColor = .green
button.setTitle("Hello", for: .normal)
button.setTitleColor(.black, for: .normal)
button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
view.addSubview(button)
let buttonSize: CGFloat = 100
button.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: view.bounds.height*3/4 - buttonSize/2, width: buttonSize, height: buttonSize)
}
@objc private func onButtonTapped() {
square.backgroundColor = .yellow
}
}
ボタンをクリックすると黄色に変化するプログラムを書けました!
さて、次に、ほんの少しだけ高度な内容についてですが、iOS開発をする上でとても重要なViewControllerのLifecycleの話をします。
例えばiPhoneでは、画面にこのような部分があります。SafeAreaといって、この部分にはアプリに大きく影響する内容のものは表示しない方がいいでしょう。
このpostに書いていますが、view.safeAreaInsets情報はUIViewControllerのlifecycleの中で得られるタイミングが決まっています。viewDidLoad( )がコールされるタイミングではまだ得られておらず、viewWillLayoutSubviews( )で得られます。
viewDidLoad( )はUIViewControllerの生成時に一度だけ呼ばれるため、view.addSubview( )などをコールする場所としては適していますが、実はframeを設定するなどレイアウト関連の作業を実施するには適していません。また、viewWillLayoutSubviews( )はlifecycleの中で状況に応じて何度もコールされるため、view.addSubview( )を実行するには適していません。
つまり、実行する内容によってコールする場所を考慮する必要がある、ということです。
文字にすると難しそうに見えますが、実際にコードを書いて説明します。以下のコードは意図の通りの結果は得られません。
class ViewController: UIViewController {
// viewWidth、topInsetはcomputed propertyです
// 何かしらの計算の結果を返すvariableのことをcomputed propertyと呼びます
var viewWidth: CGFloat {
return view.bounds.width
}
//
var topInset: CGFloat {
view.safeAreaInsets.top
}
let square: UIView = {
let square = UIView()
square.backgroundColor = .red
return square
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(square)
square.frame = CGRect(x: 0, y: topInset, width: viewWidth, height: 100)
}
}
下記のようにviewWillLayoutSubviews( )でコールすると、safeAreaInsetsの情報が反映されます。
class ViewController: UIViewController {
var viewWidth: CGFloat {
return view.bounds.width
}
var topInset: CGFloat {
view.safeAreaInsets.top
}
let square: UIView = {
let square = UIView()
square.backgroundColor = .red
return square
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(square)
}
override func viewWillLayoutSubviews() {
square.frame = CGRect(x: 0, y: topInset, width: viewWidth, height: 100)
}
}