読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

変種オセロのルール画面を作ってみた。(その4)

昨日はルールの変更に合わせてボードの修正をした。

今日は実際にルールを表示する画面を作る。

ルール画面

さっそくコードを。

//==============================
// YWF
//------------------------------
// RuleScene.swift
//==============================

import SpriteKit
import WebKit

public class RuleScene: SKScene {
  public static var scale: CGFloat = 1.0
  private static let margin: CGFloat = 20.0
  private static let headerHeight: CGFloat = 80.0
  private static let headerOffset: CGFloat = 10.0
  private static let logoHeight: CGFloat = 60.0
  private static let buttonHeight: CGFloat = 60.0
  
  private static let infoFileName: String = "RuleInfo"
  private static let infoFileExtension: String = "txt"
  private static let infoFile: String = RuleScene.infoFileName + "." + RuleScene.infoFileExtension
  
  private static let indexHTML: String = "index.html"
  private static let files: [String] = [
    "Board.png",
    "Piece.png",
    "Yoiko.png",
    "Waruiko.png",
    "Futsunoko.png",
    "Token.png",
    "InitialBoard.png",
    "8directions.png",
    "Change.png",
  ]
  
  private class func copyFilesIfNeed() -> NSURL {
    let fileManager = NSFileManager.defaultManager()
    
    let originalInfoURL = NSBundle.mainBundle().URLForResource(RuleScene.infoFileName,
                                                               withExtension: RuleScene.infoFileExtension)!
    let sourceURL = originalInfoURL.URLByDeletingLastPathComponent!
    
    let temporaryDirectoryURL = NSURL(fileURLWithPath: NSTemporaryDirectory())!
    let destURL = temporaryDirectoryURL.URLByAppendingPathComponent("www")
    let copiedInfoURL = destURL.URLByAppendingPathComponent(RuleScene.infoFile)
    fileManager.createDirectoryAtURL(destURL,
                                     withIntermediateDirectories: true,
                                     attributes: nil,
                                     error: nil)
    
    let copiedInfo: NSString! = NSString(contentsOfURL: copiedInfoURL,
                                         encoding: NSUTF8StringEncoding,
                                         error: nil)
    if copiedInfo != nil {
      let originalInfo = NSString(contentsOfURL: originalInfoURL,
                                  encoding: NSUTF8StringEncoding,
                                  error: nil)!
      if copiedInfo == originalInfo {
        // file is latest.
        return destURL.URLByAppendingPathComponent(RuleScene.indexHTML)
      } else {
        fileManager.removeItemAtURL(destURL, error: nil)
      }
    }
    
    fileManager.copyItemAtURL(sourceURL.URLByAppendingPathComponent(RuleScene.infoFile),
                              toURL: destURL.URLByAppendingPathComponent(RuleScene.infoFile),
                              error: nil)
    fileManager.copyItemAtURL(sourceURL.URLByAppendingPathComponent(RuleScene.indexHTML),
                              toURL: destURL.URLByAppendingPathComponent(RuleScene.indexHTML),
                              error: nil)
    for file in RuleScene.files {
      fileManager.copyItemAtURL(sourceURL.URLByAppendingPathComponent(file),
                                toURL: destURL.URLByAppendingPathComponent(file),
                                error: nil)
    }
    
    return destURL.URLByAppendingPathComponent(RuleScene.indexHTML)
  }
  
  private var exitButton: ButtonNode!
  private var webView: WKWebView!
  
  public override init(size: CGSize) {
    self.exitButton = nil
    self.webView = nil
    
    super.init(size: size)
    
    self.scaleMode = .AspectFill
    
    let backgroundTexture = SKTexture(imageNamed: "Background")
    let background = SKSpriteNode(texture: backgroundTexture)
    background.position = CGPoint(x: size.width/2.0, y: size.height/2.0)
    self.addChild(background)
    
    let headerTexture = SKTexture(imageNamed: "Header")
    let header = SKSpriteNode(texture: headerTexture)
    header.position = CGPoint(x: 0.0, y: self.size.height/2.0 - RuleScene.headerOffset)
    background.addChild(header)
    
    let logoTexture = SKTexture(imageNamed: "Logo")
    let logo = SKSpriteNode(texture: logoTexture)
    logo.position = CGPoint(x: (-self.size.width/2.0
                                + RuleScene.margin
                                + logo.size.width/2.0),
                            y: -RuleScene.logoHeight/2.0)
    header.addChild(logo)
    
    self.exitButton = ButtonNode(type: .Exit)
    self.exitButton.position = CGPoint(x: (self.size.width/2.0
                                           - RuleScene.margin
                                           - self.exitButton.size.width/2.0),
                                       y: -RuleScene.buttonHeight/2.0)
    //self.exitButton.addObserver(self)
    header.addChild(self.exitButton)
    
    let webBackground = SKShapeNode(rectOfSize: CGSize(width: (self.size.width
                                                               - RuleScene.margin*2.0),
                                                       height: (self.size.height
                                                                - RuleScene.headerHeight
                                                                - RuleScene.margin*2.0)))
    webBackground.lineWidth = 0.0
    webBackground.fillColor = SKColor.whiteColor().colorWithAlphaComponent(0.5)
    webBackground.position = CGPoint(x: 0.0, y: -RuleScene.headerHeight / 2.0)
    background.addChild(webBackground)
  }

  public required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  public override func didMoveToView(view: SKView) {
    // workaround for WKWebView bug.
    let indexURL = RuleScene.copyFilesIfNeed()
    
    self.webView = WKWebView(frame: CGRectZero)
    self.webView.setTranslatesAutoresizingMaskIntoConstraints(false)
    self.webView.backgroundColor = UIColor.clearColor()
    self.webView.opaque = false
    view.addSubview(self.webView)
    view.addConstraint(NSLayoutConstraint(item: self.webView,
                                          attribute: .Left,
                                          relatedBy: .Equal,
                                          toItem: view,
                                          attribute: .Left,
                                          multiplier: 1.0,
                                          constant: RuleScene.margin / RuleScene.scale))
    view.addConstraint(NSLayoutConstraint(item: self.webView,
                                          attribute: .Right,
                                          relatedBy: .Equal,
                                          toItem: view,
                                          attribute: .Right,
                                          multiplier: 1.0,
                                          constant: -RuleScene.margin / RuleScene.scale))
    view.addConstraint(NSLayoutConstraint(item: self.webView,
                                          attribute: .Top,
                                          relatedBy: .Equal,
                                          toItem: view,
                                          attribute: .Top,
                                          multiplier: 1.0,
                                          constant: (RuleScene.headerHeight + RuleScene.margin) / RuleScene.scale))
    view.addConstraint(NSLayoutConstraint(item: self.webView,
                                          attribute: .Bottom,
                                          relatedBy: .Equal,
                                          toItem: view,
                                          attribute: .Bottom,
                                          multiplier: 1.0,
                                          constant: -RuleScene.margin / RuleScene.scale))
    
    self.webView.loadRequest(NSURLRequest(URL: indexURL))
  }
  
  public override func willMoveFromView(view: SKView) {
    self.exitButton.removeAllObservers()
    self.webView.removeFromSuperview()
    self.webView = nil
  }
}

SKSceneを継承したRuleSceneというクラスを作って、ルール画面を描画するようにしている。

ただし、ルールはHTMLで用意するので、実際に表示するのはWKWebView。
そこで、シーンがビューに追加された直後に呼ばれるRuleScene#didMoveToView(_: SKView)の中でWKWebViewを作って、それをシーンを表示しているビューのサブビューにしている。

このとき、iOSでローカルのHTMLを表示する方法について。 - いものやま。で言及したとおり、WKWebViewにバグがあるので、それを回避するためにファイルのコピーを(必要なら)行っている。
また、WKWebViewのサイズをちょうどよくするために、制約(NSLayoutConstraint)を上下左右に追加している。

なお、この制約を追加するとき、まずはサブビューを親ビューに登録しないといけないみたい。
制約を設定してからサブビューを親ビューに登録しようとしたら、実行時エラーになってしまった。
あと、制約の長さをスケールで割り算しているのは、いろんな画面サイズに対応する方法について。 - いものやま。で言及した方法を使っているため、シーンの単位長とビューの単位長が変わっているから。
スケールで割り算することで、シーンでのサイズをビューでのサイズに変換してやっている。

この他にちょっと説明が必要なところとして、webBackgroundというSKShapeNodeをシーンに追加している点と、WKWebViewのbackgroundColorプロパティを透明色、opaqueプロパティをfalseにしている点があると思う。
これは何のためかというと、WKWebViewの背景色を半透明にするため。

まず、何もしない状態だと、WKWebViewの背景は不透明で、せっかく木目の背景画像を使っているのが活きてこない。
そこで、まずWKWebViewのbackgroundColorプロパティとopaqueプロパティをセットすることで、WKWebViewの背景を透明にすることが出来る。
ただ、それだと文字が読みにくいので、完全な透明ではなく、半透明にしたいところ。
そこで、webBackgroundという半透明のSKShapeNodeをシーンに追加することで、まるでWKWebViewの背景が半透明になったかのように見せることが出来る、というわけ。

動作確認

ビューコントローラをちょっと書き換えて動かしてみると、次のような感じ。

iPhone 4S (ja)
f:id:yamaimo0625:20150912140452p:plain

iPad (en)
f:id:yamaimo0625:20150912140508p:plain

iPhone 5s (ja - movie)

デバイスによって、WKWebViewのサイズがちょうどよくなっていることとか、言語設定にしたがったHTMLが表示されていることとかが分かると思う。
それと、スクロールやリンクによるジャンプが出来ていることも分かると思う。

今日はここまで!