カテゴリー
iOS Swift

iOS & Swift 101(初歩)Part 2

前回の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)
  }
}