2017年5月28日日曜日

React + Redux 非同期処理を導入

前回に引き続き, React + Redux です。 今回は前回のサンプルにajax 非同期処理を導入します。 簡単な例として実装するため今回は, ページロードの際にjsonデータを取ってきてそれをリストで描画する 例にしました。 前回のエントリーはこちら React + Redux React Router その前 React + Redux とりあえず簡単な例から サンプルは, Reddit(Redux公式より) を参考にしています。 このサンプルとの違いはデータ取得をイベントフックにしているかロード時に取得しているかの違いです。

データの準備

データはjsonファイルとして準備します。前回のプロジェクトのルートにフォルダを作成し, データを置いておきます。 このデータをwebpack-dev-serverがなんとかしてくれます。
first (プロジェクト名)
|- data
     |- data.json

data.json
{
  "data" : [
    {
      "index" : 1,
      "name" : "Taro"
    },
    {
      "index" : 2,
      "name" : "Jiro"
    }
  ]
}
これで, localhost:8080/data/data.json でデータを取得できるようになりました。

HttpClientはどうする?

React そのものには, HttpClientは含まれていません。ですので何かしらのライブラリの導入をするか, pure Javascript で頑張るかのどちらかになります。 選択肢としてはいくつかあります。 せっかく, Reactを使っているのに jQueryとか, Angularはないよな。と思いつつ。 SuperAgent, Axios などは割と使われているみたいです。 今回は, Fetchを使ってみます。
npm insatll whatwg-fetch --save

React-ThunkとMiddleware

今回のサンプルを実装するに当たって重要なのが React-Thunkと Middlewareです。 Redux Thunk (Thunk middleware for Redux.) これを使うことでAction CreatorがAction objectを返す代わりにfunctionを返すことができます。
npm install redux-thunk --save

非同期データとアクション

ここで非同期でデータを取ってくるところとアクションの関係を説明します。 データを取ってくる = データを取りに行く + データを受信した or データ取得に失敗した という形に分離できます。これでアクションは3種類? 実装する必要がありますね。 (結局このあたりの細かいところをどう設計するかが鍵になるのかと思います。最初のうちはさっぱりわからないですよね。)

サンプル

構成は前回のエントリーの続きになります。 webpackの設定などの変更はしていないです。
first
|-data
|  |- data.json
|-.babelrc
|-index.html
|-package.json
|-webpack.config.js
|-src
   |-actions
   |    |- index.jsx
   |-components
   |    |- FormDisplay.jsx
   |    |- FormInput.jsx
   |-containers
   |    |- AppContainer.jsx
   |    |- FormApp.jsx
   |    |- Page2.jsx
   |    |- Page2Container.jsx
   |-reducers
        |- fetchReducer.jsx
        |- formreducer.jsx
        |- index.jsx

index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { reducers } from './reducers/index.jsx';
import thunkMiddleware from 'redux-thunk';

import AppContainer from './containers/AppContainer.jsx';
import Page2Container from './containers/Page2Container.jsx';

const initialState = {
    formReducer: {
        value : null
    },
    fetchReducer: {
        items: []
    }
};

const store = createStore(reducers,
    initialState,
    applyMiddleware(thunkMiddleware)); // reducer, initial state

// Rendering
ReactDOM.render(
  
      
          
            
            
          
      
  ,
    document.querySelector('.content')
);
変更したのは, redux-thunk をmiddlewareの設定にしたことこれで ActionCreatorでfunctionが返せます。

reducers/fetchReducer.jsx
export function fetchReducer(state = {}, action) {
    switch(action.type) {
        case 'REQUEST_DATA_SUCCESS':
            return Object.assign({}, state, {
                items: action.data,
                lastUpdated: action.receivedAt
            });
        case 'REQUEST_DATA_FAIL': {
            return Object.assign({}, state, {
                items: action.data
            });
        }
        default:
            return state;
    }
}
items というデータのArrayを返します。これはデータのリクエストが成功した時(その時はデータ) 、失敗した時に返します(その時は空[])

reducers/index.jsx
import { combineReducers } from 'redux';
import { formReducer } from './formreducer.jsx';
import { fetchReducer} from './fetchReducer.jsx';

export const reducers = combineReducers({
    formReducer,
    fetchReducer
});
前回作成した、reducerと共に利用するので2つのReducerをcombineReducersで 繋げます。

actions/index.jsx
const SEND = 'SEND';

/* Action Creator */
// Return Object
export function send(value) {
    // Action
    return {
        type: SEND,
        value,
    };
}

/* Action2 */

const REQUEST_DATA = 'REQUEST_DATA';
const REQUEST_DATA_SUCCESS = 'REQUEST_DATA_SUCCESS';
const REQUEST_DATA_FAIL = 'REQUEST_DATA_FAIL';

function invalidateData() {
    return {
        type: REQUEST_DATA_FAIL,
        data: []
    }
}

function requestData() {
    return {
        type: REQUEST_DATA
    }
}

function receiveData(json) {
    return {
        type: REQUEST_DATA_SUCCESS,
        data : json,
        receivedAt: Date.now()
    }
}

export function fetchData() {
    return function (dispatch) {
        dispatch(requestData());
        return fetch('/data/data.json')
            .then(response => response.json())
            .then(json => dispatch(receiveData(json.data)))
            .catch(e =>
                dispatch(invalidateData()));
    }
}
前回作成したActionCreatorに3つのActionを追加しました。 データ取得できなかった場合のAction, リクエスト開始時のAction, データが取得できた場合のActionです。これらのアクションをまとめた fetchデータというfunctionをexportしています。

containers/Page2.jsx
import React from 'react';
import PropTypes from 'prop-types';
import {fetchData} from '../actions/index.jsx';

class Page2 extends React.Component {

    constructor(props) {
        super(props);
    }

    componentDidMount() {
       const { dispatch } = this.props;
    //    console.log(dispatch);
       dispatch(fetchData());
    }

    render() {
        const { items } = this.props;

        var list = [];
        for(var index in items){
            list.push(
  • {items[index].name}
  • ); } return (
      {list}
    ); } } Page2.propTypes = { items: PropTypes.array }; export { Page2 as default };
    componentDidMountの所でデータを非同期に取得しています。renderの部分データを取り出し リストを作成して描画します。
    最後は Containerですね。 containers/Page2Container.jsx
    // Connect to Redux
    function mapStateToProps(state) {
        return {
            items: state.fetchReducer.items
        }
    }
    
    const Page2Container = connect(
        mapStateToProps
    )(Page2)
    
    export { Page2Container as default };
    
    この部分でPropertyとComponentを繋ぎます。

    テスト

    動作確認
    webpack-dev-server
    
    localhost:8080/member にアクセス。2つのアイテムのあるリストが見えるはずです。

    React + Redux React Router

    前回に引き続き, React + Reduxを見ていきます。 今回は 前回のサンプルにReact Routerを導入します。 前回のサンプルはこちらをご覧ください。 (React + Redux とりあえず簡単な例から)

    React Routerを導入

    npm install --save react-router-dom
    
    Versionは, 4.1.1 を使用。このバージョンが曲者っぽい。 4以下の書き方などは違う模様。 また, react-router というパッケージと react-router-dom は違うみたい。 前はreact-routerを使っていた気がするんだけどなぁ。 今回は, react-router-domを使います。

    Container の作成

    今回はもう1ページ用にもう一つContainerを作成します。Reduxの流れを作るのが面倒なので 簡単なComponentにしておきます。
    containers/Page2.jsx
    class Page2 extends React.Component {
       render() {
            return (
                
    Hello!
    ); } } export { Page2 as default };
    ただのSimpleなdivです。

    React-Routerを入れる

    前回作った部分を/appとして今回の部分を/memberと二つにします。 (コードは一部のみ)
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
    
    import AppContainer from './containers/AppContainer.jsx';
    import Page2 from './containers/Page2.jsx';
    
    
    ReactDOM.render(
      
          
              
                
                
              
          
      ,
        document.querySelector('.content')
    );
    
    前回と違うところは Prividerのしたのところに, RouterというタグとSwitchというタグ, その中に各ページの定義 Routeが入っています。 これで localhost:8080/app, localhost:8080/member で各ページが確認できます。

    React + Redux とりあえず簡単な例から

    久々の投稿。ここしばらくは, モバイル, フロントがメインでたまにバックエンドの開発をする日が続いています。
    仕事では, Angular 1 がメインでしたそろそろReact + webpackでいこうと模索しています。
    React をしばらく触って, それからReact + Redux の学習を初めています。
    React は本読んだりしてなんとかしましたが, redux にはかなり苦労しています。


    参考にしたもの

    一番上の解説はわかりやすかったです。今回のサンプルもここで取り上げていたサンプルをベースに作っています。 下2つはReduxの公式のサンプルです。フォルダの構成とか結構参考になると思います。

    最終的なゴール

    • (今回)webpackでビルドする
    • (今回)webpack-dev-server で動作確認
    • (今回)reduxの導入
    • (今回)es6でかけるようにする
    • (今回)action, container, component, reducerの構成を作る
    • react-routerを使う
    • ajaxでデータを取ってくる
    react-routerと, ajaxに関しては次回以降取り上げたいと思います。

    バージョンなど

    作成日 : 2017/05/28 webpack : 2.5.1
    プロジェクトの作成, react, webapack, redux の導入
    mkdir first     
    cd first
    npm init -y
    npm install --save webpack webpack-dev-server
    npm install --save-dev babel babel-core babel-loader babel-preset-es2015 babel-preset-react
    npm install --save react react-dom react-redux redux
    npm install --save prop-types
    
    firstはプロジェクト名です。なんでも良いです。 今回は, webpack, webpack-dev-server, react, react-redux, reactのプロパティを使うとWarningが出るので prop-typesを使います。

    フォルダ構成
    first
    |- dist
    |- src
    |   |- actions
    |   |- components
    |   |- containers
    |   |- reducers
    |   |- index.jsx
    |- .babelrc
    |- index.html
    |- package.json
    |- wbpack.config.js
    


    .babelrc

    ここで, es6を使うための設定をします。
    {
      "presets": [
        "es2015", "react"
      ]
    }
    


    webpack.config.js

    webpack の設定です。今回は jsx を動作させるための設定だけを記載しました。
    const path = require('path');
    
    module.exports = {
        entry: path.join(__dirname, 'src/index.jsx'),
        output: {
            path: path.join(__dirname, 'dist'),
            filename: 'bundle.js'
        },
        devServer: {
            contentBase: '.',
            port: 8080,
            inline: true,
            historyApiFallback: true
        },
        module: {
            loaders: [
                {
                    test: /\.jsx$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader'
                }
            ]
        }
    };
    
    webpackでビルドした結果を distフォルダへ, babel-loaderを使って, jsxを処理します。 webpack-dev-server はポート8080で動作させます。

    Redux

    Redux に関しては他のブログなどのエントリーを参考にしてさらっとまとめます。 Redux : Redux 細かい話は公式などを確認した方が良さそう。 状態管理をアプリ全体で行う, 状態管理するStoreはアプリ全体で1つ。 ユーザのActionがコールされると, Recuderを使って, Storeの状態を更新する。 ここで登場人物が3つ出てきました。(Action, Reducer, Store) Reduxではデータフローが重要で, データフローのルールに従います。 以下各要素の説明とプログラム上での登場人物の整理です。

    Action

    typeをキーとしてStateを更新するための定義を書く。 状態更新はdispatch関数を実行して行う (ajaxの通信はここで行う) ActionはAction名(Reducerで処理を判別するため)と状態の値を持った単なるオブジェクト Storeのdispatch() メソッドの引数にAction Creatorを渡すことでActionがReducerに送られる

    ActionCreator

    最終的には, Actionと同じファイルに書いている functionで Action = object を返す Actionで書いていない更新したい状態の定義を書く。 Stateの更新自体は, Reducerがするので更新したい部分を引数などから生成する

    Reducer

    Reducerは、現在の状態(state)と受け取ったActionを引数に取り、新しい状態を返す関数 typeをキーにして処理を分ける, Switch 文中に Action で定義した State の type で分岐しロジックを書く stateの更新には, ES2015 Object.assign()を使おう!! Object.assign() はmergeオブジェクトのマージを行います。 ※Reducerは複数作ることができます。(ページごとに作るイメージかなぁ) (storeは一つしか引数に取れないので combineReducersで combine)

    Store

    状態管理のオブジェクト 初期データと, ReducerからcreateStoreメソッドで作成します。これをProvider渡します。 Reduxのmiddlewareを渡す場合はそれも引数に加えます。

    Container Components

    親コンポーネント。ここでReactとReduxのつなぎ込みを行います。 Container ComponentsはPresentational Componentを持ちます。

    Presentational Component

    子コンポーネント。最小単位のコンポーネント 親からプロパティを受け取り描画します。イベントなども親から受け取ります。

    サンプル

    今回のサンプルは参考の一番上のエントリーを参考にreact + reduxをwebpackで動作させる, フォルダ構成をReactの公式サンプルを 元に作り変えたものです。 index.jsx エントリー部分。
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    import { createStore } from 'redux';
    import { Provider } from 'react-redux';
    import { reducers } from './reducers/index.jsx';
    
    import AppContainer from './containers/AppContainer.jsx';
    
    const initialState = {
        formReducer: {
            value : null
        }
    };
    
    const store = createStore(reducers,
        initialState); // reducer, initial state
    
    // Rendering
    ReactDOM.render(
      
          
      ,
        document.querySelector('.content')
    );
    


    とりあえず必要なComponentなどを全部importしています。 ここに直接書くのは, 初期状態の設定と(この部分もファイル分離できる) store の作成と, レンダリングの部分ですね。 Reactに連携させるReduxのStoreを渡しています。Providerという部分でReactとReduxのStoreの連携をしていますね。

    index.html 元になるView,ただし動作関係は全部上のindex.jsxに任せる
    
    
    
        
        Redux Sample
    
    
    
    ビルドした後のjsを取り込んでいるだけです。この部分はもう基本的には変更しません。

    actions/index.jsx アクションの実装です。Actionと,ActionCreatorの実装をしています。
    const SEND = 'SEND';
    
    /* Action Creator */
    // Return Object
    export function send(value) {
        // Action
        return {
            type: SEND,
            value,
        };
    }
    
    Actionは, objectを返します。 ここでは, Actionタイプ(アクションの名前みたいなもの)とアクションを発行した部分から valueという値を受け取ってそれを返しています。

    recucers/formreducer.jsx Reducerの実装です。
    export function formReducer(state = {}, action) {
        switch(action.type) {
            case 'SEND':
                return Object.assign({}, state, {
                    value: action.value,
                });
            default:
                return state;
        }
    }
    


    containers/FormApp.jsx
    import React from 'react';
    import PropTypes from 'prop-types';
    
    import FormInput from '../components/FormInput.jsx';
    import FormDisplay from '../components/FormDisplay.jsx';
    
    class FormApp extends React.Component {
        render() {
            return (
                
    ); } } FormApp.propTypes = { onClick: PropTypes.func.isRequired, value: PropTypes.string }; export { FormApp as default };


    containers/AppContainer.jsx
    import { connect } from 'react-redux';
    import { send } from '../actions/index.jsx';
    import FormApp from './FormApp.jsx';
    
    // Connect to Redux
    function mapStateToProps(state) {
        return {
            //value: state.value
            value: state.formReducer.value    //http://qiita.com/usagi-f/items/ae568fb64c2eac882d05
        }
    }
    
    function mapDispatchToProps(dispatch) {
        return {
            onClick(value) {
                dispatch(send(value));
            }
        };
    }
    
    const AppContainer = connect(
        mapStateToProps,
        mapDispatchToProps
    )(FormApp)
    
    export { AppContainer as default };
    
    ここで Redux のプロパティとComponentを繋ぐ作業をしています。 storeからデータをひっぱてくるところ, メソッドを渡すとこですね。

    components/FormDisplay.jsx
    class FormDisplay extends React.Component {
        render() {
            return (
                
    {this.props.data}
    ); } }; FormDisplay.propTypes = { data: PropTypes.string }; export { FormDisplay as default };


    components/FormInput.jsx
    class FormInput extends React.Component {
        send(e) {
            e.preventDefault();
            this.props.handleClick(this.myInput.value.trim());
            this.myInput.value = '';
            return;
        }
        render() {
            return(
                
    (this.myInput = ref)} defaultValue="" />
    ); } }; FormInput.propTypes = { handleClick: PropTypes.func.isRequired }; export { FormInput as default };


    動作確認

    webpackでビルド, webpack-dev-serverで簡易サーバーを動かします。 ビルド
    webpack
    
    サーバーを動かす
    webpack-dev-server
    
    localhost:8080/index.html へアクセス

    2015年1月17日土曜日

    Swift UICollectionViewをコードでつくる

    UICollectionViewをコードでつくります

    UICollectionViewDelegate, UICollectionViewDataSource UICollectionViewDelegateFlowLayout をUIViewController 内で実装します UIは, xibや, storyboardを使わずすべてコードで実装します セルやフッターなどのUIコードは省略します.
    class ViewController : UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
        private let barSize : CGFloat = 44.0
        private let kCellReuse : String = "PackCell"
        private let kCellheaderReuse : String = "PackHeader"
        private var collectionView : UICollectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: UICollectionViewFlowLayout())   // Initialization
     
        override func viewDidLoad() {
            super.viewDidLoad()
     
            // Collection
            self.collectionView.delegate = self     // delegate  :  UICollectionViewDelegate
            self.collectionView.dataSource = self   // datasource  : UICollectionViewDataSource
            self.collectionView.backgroundColor = UIColor.clearColor()
     
            // Register parts(header and cell
            self.collectionView.registerClass(PackViewCell.self, forCellWithReuseIdentifier: kCellReuse) // UICollectionViewCell
            self.collectionView.registerClass(PackCollectionSectionView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: kCellheaderReuse)  // UICollectionReusableView
             
            self.view.addSubview(self.collectionView)
        }
     
        override func viewWillLayoutSubviews() {
            let frame = self.view.frame
            self.collectionView.frame = CGRectMake(frame.origin.x, frame.origin.y + barSize, frame.size.width, frame.size.height - barSize)
        }
     
        // MARK: UICollectionViewDelegate, UICollectionViewDataSource
        func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
            var cell : PackViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(kCellReuse, forIndexPath: indexPath) as PackViewCell
            return cell    // Create UICollectionViewCell
        }
     
        func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
            return 1  // Number of section
        }
     
        func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            var res = 0
            switch(section) {
            case 0:
                res = 4  // Number of cell per section(section 0)
                break
            default:
                res = 0
                break
            }
            return res
        }
     
        func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
            collectionView.deselectItemAtIndexPath(indexPath, animated: false) 
            // Select operation
        }
     
        func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
            var reusableView : UICollectionReusableView? = nil
             
            // Create header
            if (kind == UICollectionElementKindSectionHeader) {
                // Create Header
                var headerView : PackCollectionSectionView = collectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: kCellheaderReuse, forIndexPath: indexPath) as PackCollectionSectionView
                 
                reusableView = headerView
            }
            return reusableView!
        }
     
        // MARK: UICollectionViewDelegateFlowLayout
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
            return CGSize(width: 90, height: 90) // The size of one cell
        }
     
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
            return CGSizeMake(self.view.frame.width, 90)  // Header size
        }
     
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
            let frame : CGRect = self.view.frame
            let margin  = (frame.width - 90 * 3) / 6.0
            return UIEdgeInsetsMake(10, margin, 10, margin) // margin between cells
        }
    }
    
    UICollectionViewDelegateFlowLayout の実装でセルのサイズや, ヘッダーのサイズ, セル間のマージンなどを決めています UICollectionViewDelegate, 実装していれば読み込まれるはずです コードそのものは, UITableViewの実装によく似ています. 行やセクションのナンバーから, セルを引っ張りだすことなど 違いは , レイアウトが柔軟なこと, サイズを返すところで行ごとにサイズを変更できます

    2014年12月20日土曜日

    WKWebView アラート(alert)を表示させる

    省メモリや高機能が売りの, WKWebViewですが, いろいろ問題があるようです
    alertの表示もその一つです
    Webページ上で, JavaScriptでalert(); などのメソッドでダイアログを表示させようとすると, WKWebViewでは無反応になります
    表示させる場合,タイプに応じて自前でコントロールを実装してあげる必要があります
    カスタマイズができるので, 「いい」とも思われますが, 面倒ですよね
    オリジナルはこちら(English), WkWebView Prompt 英語版の方には, 検索で見つけたテスト用のページのリンクを貼っておきました

    WKUIDelegateを実装する

    個人的な推奨ですが, これらのメソッドをUIViewControllerに実装して, WKWebViewを利用するUIViewControllerに
    継承させれば, 楽かなと思っています

    UIViewController.h
    @interface ViewController : UIViewController
    
    @end
    


    UIViewController.m
    @implementation ViewController
    
    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)())completionHandler {
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:webView.URL.host message:message preferredStyle:UIAlertControllerStyleAlert];
         
        [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
            completionHandler();
        }]];
        [self presentViewController:alertController animated:YES completion:nil];
    }
     
    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
         
        // TODO We have to think message to confirm "YES"
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:webView.URL.host message:message preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
            completionHandler(YES);
        }]];
        [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
            completionHandler(NO);
        }]];
        [self presentViewController:alertController animated:YES completion:nil];
    }
     
    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {
         
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:webView.URL.host preferredStyle:UIAlertControllerStyleAlert];
        [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
            textField.text = defaultText;
        }];
         
        [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
            NSString *input = ((UITextField *)alertController.textFields.firstObject).text;
            completionHandler(input);
        }]];
         
        [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
            completionHandler(nil);
        }]];
        [self presentViewController:alertController animated:YES completion:nil];
    }
    
    
    @end
    
    この3つのメソッドの実装で, アラート, 選択式アラート, 入力ボックス付きアラートはカバーできます
    ローカライズ言語部分は省略しています.
    適宜置き換えてください

    WKWebView スクリーンショット

    UIWebViewで簡単に取得できたスクリーンショットも, WKWebViewでは同じ方法ではできません.

    オリジナルはこちら (English),WKWebView Screenshots

    UIViewのみを取得したい場合

    snapshotViewAfterScreenUpdates を利用します。これだけで, UIViewまではO.K.です Objective-C
    UIView *capturedView = [self snapshotViewAfterScreenUpdates:NO];
    
    swift
    var capturedView : UIView? = self.snapshotViewAfterScreenUpdates(false)
    
    このUIViewは, 通常の方法でUIImageに変換できません。 drawViewHierarchyInRectを使うと, 常に NO, false が返ってきます UIViewを, Contextに描画できないようです シミュレータを使うと, drawViewHierarchyInRectでも描画できますが, 実機だと上記のようなことが起こります(iPhone5s, iPhone4s)

    UIImageにしたい場合

    上の方法だと実機で, UIView, UIImageのコンバートに失敗します ScrollViewの内容をそのまま描画してImageにしてしまえば, 実機でもうまくいきます
    - (UIImage *)screenCapture {
        CGSize size = self.scrollView.contentSize;
        UIGraphicsBeginImageContextWithOptions(size, YES, 0);
        [self drawViewHierarchyInRect:CGRectMake(self.bounds.origin.x, self.bounds.origin.y, size.width, size.height) afterScreenUpdates:YES];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
     
    - (UIImage *)screenCapture:(CGSize)size {
        UIGraphicsBeginImageContext(size);
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        CGFloat scale = size.width / self.layer.bounds.size.width;
        CGAffineTransform transform = CGAffineTransformMakeScale(scale, scale);
        CGContextConcatCTM(ctx, transform);
        [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:NO];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
    

    2014年11月22日土曜日

    iOS WebView(UIWebView, WKWebView)をバージョンで分けて利用する

    WebView

    iOSでWebページを表示させる場合, UIWebViewを利用します. しかし, iOS8から, WKWebViewが登場しました.
    WKWebViewは, Safariに搭載されている機能と同様の機能を含んでおり, Safariに似た,機能を利用できます.
    またパフォーマンス面でも, WKWebViewは, UIWebViewを上回っています.
    ですが, この機能は, WebKit というFrameworkを利用しており, これは, iOS8より利用できるので, iOS7以下ではりようできません そこで, OSのバージョンによりUIWebView, WKWebViewを分ければ, iOS8によりよい機能を提供できるのでは.

    サンプル

    UIWebView, WKWebViewをハンドルクラスでラップして, iOS7,8で使い分けられるようにする
    UIWebViewの機能をベースにする
    Delegateは, 外で実装, delegateのセットは内部で行う(WKWebViewは, Navigationのみ)
    storyboardを使わずコードのみで実装


    サンプルの構成



    WebHandler : iOS7, iOS8で, UIWebView, WKWebViewを分ける, 機能もそれぞれ分ける
    そのほかのView
    カテゴリ : UIWebView+Info.m : UIWebViewで, タイトルや, URLを取得するための拡張機能 コードは, 一部省略しています(AppDelegate.mなど)

    UIWebView+Info.h

    #import 
    
    @interface UIWebView (Info)
    
    - (NSString *)title;
    - (NSString *)URLString;
    - (NSURL *)URL;
    
    @end
    

    UIWebView+Info.m

    #import "UIWebView+Info.h"
    
    @implementation UIWebView (Info)
    
    - (NSString *)title {
        return [self stringByEvaluatingJavaScriptFromString:@"document.title"];
    }
    
    - (NSString *)URLString {
        return [self stringByEvaluatingJavaScriptFromString:@"document.URL"];
    }
    
    - (NSURL *)URL {
        return [NSURL URLWithString:self.URLString];
    }
    
    @end
    


    WebHandler.h

    #import 
    #import 
    
    typedef void (^CompletionBlock)(id, NSError*);
    
    @interface WebHandler : NSObject
    
    - (id)webView;
    - (void)layout:(CGRect)frame;
    - (void)setDelegate:(id)delegate;
    - (void)clean;
    
    // Load
    - (void)loadRequest:(NSURLRequest *)request;
    - (void)loadURL:(NSString *)url;
    - (void)stopLoading;
    
    // Web history
    - (void)goBack;
    - (void)goForward;
    - (BOOL)canGoBack;
    - (BOOL)canGoForward;
    - (BOOL)isLoading;
    
    // JavaScript
    - (void)stringByEvaluatingJavaScriptFromString:(NSString *)javascript completion:(CompletionBlock)completion;
    
    // Info
    - (NSString *)title;
    - (NSURL *)URL;
    
    @end
    


    WebHandler.m

    #import "WebHandler.h"
    #import 
    #import "UIWebView+Info.h"
    
    @interface WebHandler()
    
    @property (nonatomic) UIWebView *uiwebView;
    @property (nonatomic) WKWebView *wkwebView;
    @property (nonatomic) BOOL ios8;
    
    @end
    
    @implementation WebHandler
    
    - (id)init {
        if (self = [super init]) {
            self.ios8 = [WKWebView class] != nil;
            if (self.ios8) {
                self.wkwebView = [[WKWebView alloc] init];
            }
            else {
                self.uiwebView = [[UIWebView alloc] init];
            }
        }
        return self;
    }
    
    - (id)webView {
        if (self.ios8) {
            return self.wkwebView;
        }
        return self.uiwebView;
    }
    
    - (void)layout:(CGRect)frame {
        if (self.ios8) {
            self.wkwebView.frame = frame;
    
        }
        else {
            self.uiwebView.frame = frame;
        }
    }
    
    - (void)setDelegate:(id)delegate {  // This is for Navigation Delegate
        if (self.ios8) {
            self.wkwebView.navigationDelegate = delegate;
        }
        else {
            self.uiwebView.delegate = delegate;
        }
    }
    
    - (void)setUIDelegate:(id)delegate {  // This is for only ios8
        if (self.ios8) {
            self.wkwebView.UIDelegate = delegate;
        }
    }
    
    - (void)clean {
        if (self.ios8) {
            self.wkwebView.navigationDelegate = nil;
        }
        else {
            self.uiwebView.delegate = nil;
        }
        
        if ([self isLoading]) {
            [self stopLoading];
        }
    }
    
    
    #pragma mark - Common Features
    
    /*
     * Load
     */
    - (void)loadRequest:(NSURLRequest *)request {
        if (self.ios8) {
            [self.wkwebView loadRequest:request];
        }
        else {
            [self.uiwebView loadRequest:request];
        }
    }
    
    - (void)loadURL:(NSString *)url {
        [self loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
    }
    
    - (void)stopLoading {
        if (self.ios8) {
            [self.wkwebView stopLoading];
        }
        else {
            [self.uiwebView stopLoading];
        }
    }
    
    /*
     *  History
     */
    - (void)goBack {
        if (self.ios8) {
            [self.wkwebView goBack];
        }
        else {
            [self.uiwebView goBack];
        }
    }
    
    - (void)goForward {
        if (self.ios8) {
            [self.wkwebView goForward];
        }
        else {
            [self.uiwebView goForward];
        }
    }
    
    - (BOOL)canGoBack {
        if (self.ios8) {
            return [self.wkwebView canGoBack];
        }
        else {
            return [self.uiwebView canGoBack];
        }
    }
    
    - (BOOL)canGoForward {
        if (self.ios8) {
            return [self.wkwebView canGoForward];
        }
        else {
            return [self.uiwebView canGoForward];
        }
    }
    
    - (BOOL)isLoading {
        if (self.ios8) {
            return [self.wkwebView isLoading];
        }
        else {
            return [self.uiwebView isLoading];
        }
    }
    
    // goToBackForwardListItem is only for iOS8 WKWebView
    
    #pragma mark - JavaScript
    - (void)stringByEvaluatingJavaScriptFromString:(NSString *)javascript completion:(CompletionBlock)completion {
        if (self.ios8) {
            [self.wkwebView evaluateJavaScript:javascript completionHandler:completion];
        }
        else {
            [self.uiwebView stringByEvaluatingJavaScriptFromString:javascript];
        }
    }
    
    
    #pragma mark - Info
    /*
     * Info
     */
    - (NSString *)title {
        if (self.ios8) {
            return [self.wkwebView title];
        }
        else {
            return [self.webView title];
        }
    }
    
    - (NSURL *)URL {
        if (self.ios8) {
            return [self.wkwebView URL];
        }
        else {
            return [self.webView URL];
        }
    }
    @end
    


    UIHeaderView.h

    #import 
    
    @interface UIHeaderView : UIView
    
    @property (nonatomic) UILabel *titleLabel;
    
    @end
    


    UIHeaderView.m

    #import "UIHeaderView.h"
    
    @implementation UIHeaderView
    
    - (id)init {
        if(self = [super init]) {
            [self setBackgroundColor:[UIColor grayColor]];
            
            self.titleLabel = [[UILabel alloc] init];
            [self.titleLabel setTextColor:[UIColor whiteColor]];
            [self.titleLabel setTextAlignment:NSTextAlignmentCenter];
            [self addSubview:self.titleLabel];
        }
        return self;
    }
    
    - (void)layoutSubviews {
        self.titleLabel.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
    }
    
    @end
    


    UIFooterView.h

    import 
    
    @interface UIFooterView : UIView
    
    @property (nonatomic) UIButton *backButton;
    @property (nonatomic) UIButton *forwardButton;
    
    @end
    


    UIFooterView.m

    #import "UIFooterView.h"
    
    #define kBUTTONSIZE 80
    
    @implementation UIFooterView
    
    - (id)init {
        if(self = [super init]) {
            [self setBackgroundColor:[UIColor grayColor]];
            self.backButton = [[UIButton alloc] init];
            [self.backButton setTitle:@"Back" forState:UIControlStateNormal];
            [self.backButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
            
            self.forwardButton = [[UIButton alloc] init];
            [self.forwardButton setTitle:@"Forward" forState:UIControlStateNormal];
            [self.forwardButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
            
            [self addSubview:self.backButton];
            [self addSubview:self.forwardButton];
        }
        return self;
    }
    
    - (void)layoutSubviews {
        CGRect viewRect = self.frame;
        self.backButton.frame = CGRectMake(0, 0, kBUTTONSIZE, viewRect.size.height);
        self.forwardButton.frame = CGRectMake(0 + kBUTTONSIZE + 10, 0, kBUTTONSIZE, viewRect.size.height);
    }
    @end
    


    ViewController.h

    #import 
    #import 
    
    @interface ViewController : UIViewController
    
    @end
    

    ViewController.m

    #import "ViewController.h"
    #import "WebHandler.h"
    #import "UIHeaderView.h"
    #import "UIFooterView.h"
    
    #define kHEADERHEIGHT 44
    #define kFOOTERHEIGHT 44
    
    @interface ViewController ()
    
    @property (nonatomic) WebHandler *webhandler;
    @property (nonatomic) UIHeaderView *header;
    @property (nonatomic) UIFooterView *footer;
    @property (nonatomic) int webCount;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.view.backgroundColor = [UIColor whiteColor];
        
        // Web
        self.webhandler = [[WebHandler alloc] init];
        [self.webhandler setDelegate:self];
        
        self.webCount = 0;
        
        // Header
        self.header = [[UIHeaderView alloc] init];
        
        // Footer
        self.footer = [[UIFooterView alloc] init];
        [self.footer.backButton addTarget:self action:@selector(backClick:) forControlEvents:UIControlEventTouchUpInside];
        [self.footer.forwardButton addTarget:self action:@selector(forwardClick:) forControlEvents:UIControlEventTouchUpInside];
        
        [self.view addSubview:self.header];
        [self.view addSubview:self.webhandler.webView];
        [self.view addSubview:self.footer];
        
        [self.webhandler loadURL:@"http://google.com.sg"];
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
       
        if (self.isViewLoaded && [self.view window] == nil) {
            [self dispose];
            self.view = nil;
        }
    }
    
    - (void)dealloc {
        [self dispose];
    }
    
    - (void)dispose {
        if (self.webhandler != nil) {
            [self.webhandler clean];
        }
    }
    
    - (void) viewWillLayoutSubviews {
        
        // Calculate layout
        CGRect viewRect = self.view.frame;
        self.header.frame = CGRectMake(viewRect.origin.x,
                                       viewRect.origin.y,
                                       viewRect.size.width,
                                       kHEADERHEIGHT);
        [self.webhandler layout:CGRectMake(viewRect.origin.x,
                                          viewRect.origin.y + kHEADERHEIGHT,
                                          viewRect.size.width,
                                          viewRect.size.height - kHEADERHEIGHT - kFOOTERHEIGHT)];
        
        self.footer.frame = CGRectMake(viewRect.origin.x,
                                       viewRect.origin.y + viewRect.size.height - kFOOTERHEIGHT,
                                       viewRect.size.width,
                                       kFOOTERHEIGHT);
        
    }
    
    #pragma mark - UIWebViewDelegate(iOS7)
    - (void)webViewDidStartLoad:(UIWebView *)webView {
        // Start Loading
        NSLog(@"Start iOS7");
        self.webCount++;
    }
    
    - (void)webViewDidFinishLoad:(UIWebView *)webView {
        self.webCount--;
        if (self.webCount == 0) {
            NSLog(@"Finish iOS7");
            [self.header.titleLabel setText:[self.webhandler title]];
        }
    }
    
    - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
        NSLog(@"Erorr iOS7");
    }
    
    #pragma mark - WKWebViewDelegate(iOS8)
    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
        NSLog(@"Start iOS8");
    }
    
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
        [self.header.titleLabel setText:[self.webhandler title]];
        NSLog(@"title %@", [self.webhandler title]);
        NSLog(@"Finish iOS8");
    }
    
    - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
        NSLog(@"Error iOS8");
    }
    
    #pragma mark - Button Click
    - (void)backClick:(id)sender {
        if ([self.webhandler canGoBack]) {
            [self.webhandler goBack];
        }
    }
    
    - (void)forwardClick:(id)sender {
        if ([self.webhandler canGoForward]) {
            [self.webhandler goForward];
        }
    }
    @end
    


    実行結果

    iOS7, iOS8 ともに, 同じ結果が得られます


    リファレンス

    NS(WKWebView)
    Professional Programmer