使用Elm构建iOS应用程序

使用Elm构建iOS应用程序。 使用本机或Web UI。 或混合搭配

Swift 其它杂项

访问GitHub主页

共185Star

详细介绍

Elm for iOS

Build iOS apps with Elm. Use native or web UI. Or mix and match.

Elm Logo Swift Logo

Continuous integration status:

master develop
BuddyBuild BuddyBuild

Example

Let's build a counter:

Screenshot

Functional core

  • 100% platform independent
  • 100% type safe
  • 100% purely functional
port module Counter exposing (..)


type alias Model =
    Int


model : Model
model =
    0


type alias Flags =
    { initialCount : Int }


init : Flags -> ( Model, Cmd Msg )
init flags =
    let
        initialModel =
            flags.initialCount
    in
        ( initialModel, view initialModel )


type Msg
    = Increment
    | Decrement


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    let
        newModel =
            case msg of
                Increment ->
                    model + 1

                Decrement ->
                    model - 1

        command =
            view newModel
    in
        ( newModel, command )


type alias View =
    { count : String }


view : Model -> Cmd Msg
view model =
    setCountLabelText (toString model)


port userDidTapIncrementButton : (() -> msg) -> Sub msg


port userDidTapDecrementButton : (() -> msg) -> Sub msg


port setCountLabelText : String -> Cmd msg


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.batch
        [ userDidTapIncrementButton (\_ -> Increment)
        , userDidTapDecrementButton (\_ -> Decrement)
        ]


main : Program Flags Model Msg
main =
    Platform.programWithFlags
        { init = init
        , update = update
        , subscriptions = subscriptions
        }

Tests:

module Tests exposing (..)

import Test exposing (..)
import Expect
import Counter exposing (..)


model : ( Model, Cmd Msg ) -> Model
model =
    Tuple.first


command : ( Model, Cmd Msg ) -> Cmd Msg
command =
    Tuple.second


all : Test
all =
    describe "Update"
        [ describe "Increment"
            [ test "Change model 1" <|
                \() ->
                    update Increment 1
                        |> model
                        |> Expect.equal 2
            , test "Change model 2" <|
                \() ->
                    update Increment 2
                        |> model
                        |> Expect.equal 3
            , test "Send command 1" <|
                \() ->
                    update Increment 1
                        |> command
                        |> Expect.equal (setCountLabelText "2")
            , test "Send command 2" <|
                \() ->
                    update Increment 2
                        |> command
                        |> Expect.equal (setCountLabelText "3")
            ]
        , describe "Decrement"
            [ test "Change model 1" <|
                \() ->
                    update Decrement -1
                        |> model
                        |> Expect.equal -2
            , test "Change model 2" <|
                \() ->
                    update Decrement -2
                        |> model
                        |> Expect.equal -3
            , test "Send command 1" <|
                \() ->
                    update Decrement -1
                        |> command
                        |> Expect.equal (setCountLabelText "-2")
            , test "Send command 2" <|
                \() ->
                    update Decrement -2
                        |> command
                        |> Expect.equal (setCountLabelText "-3")
            ]
        ]

Imperative shell

Storyboard

  • Platform dependent
  • Divided into two parts — safe and unsafe

Safe part:

import UIKit
import Elm

final class ViewController: UIViewController, Elm {

    @IBOutlet var countLabel: UILabel?

    struct Flags {
        let initialCount: Int
    }

    enum Input {
        case userDidTapIncrementButton
        case userDidTapDecrementButton
    }

    enum Output {
        case setCountLabelText(String)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        start(module: "counter", flags: .init(initialCount: 0))
    }

    @IBAction func userDidTapIncrementButton() {
        send(.userDidTapIncrementButton)
    }

    @IBAction func userDidTapDecrementButton() {
        send(.userDidTapDecrementButton)
    }

    func observe(_ output: Output) {
        switch output {
        case .setCountLabelText(let text):
            countLabel?.text = text
        }
    }

}

Unsafe part where we convert data between Swift to JavaScript:

extension ViewController.Flags: ElmValue {
    var javaScript: String {
        return ["initialCount": initialCount].javaScript
    }
}

extension ViewController.Input: ElmInput {
    var port: (ElmPort, ElmValue?) {
        switch self {
        case .userDidTapIncrementButton:
            return ("userDidTapIncrementButton", nil)
        case .userDidTapDecrementButton:
            return ("userDidTapDecrementButton", nil)
        }
    }
}

extension ViewController.Output: ElmOutput {
    init(port: ElmPort, value: ElmValue) {
        switch port {
        case "setCountLabelText":
            let value = value as! String
            self = .setCountLabelText(value)
        default:
            fatalError()
        }
    }
}

Build

Add the following Run Script build phase to compile Elm during Xcode build:

elm make "$SRCROOT"/Elm/src/Counter.elm --output "$CONFIGURATION_BUILD_DIR"/"$UNLOCALIZED_RESOURCES_FOLDER_PATH"/counter.js --yes

Notes

Due to this bug, all port modules need to include:

import Json.Decode

Thanks

Questions? Comments? Concerns?

Say hello!