uikit

0.1.2


UIKit library for clojure-objc

dependencies

galdolber/clojure-objc
1.6.0



(this space intentionally left almost blank)
 
(ns uikit.core)
(defn- nsdictionary [map]
  (when map
    (let [d ($ ($ NSMutableDictionary) :new)]
      (doseq [[k v] map]
        ($ d :setObject v :forKey (name k)))
      d)))
(def default-center ($ ($ NSNotificationCenter) :defaultCenter))

Adds an observer for a notification.

The handler is retained.

(defn add-observer
  ([target handler n] (add-observer target handler "invokeWithId:" n))
  ([target handler selector n]
     ($ default-center
        :addObserver ($ handler :retain)
        :selector (sel selector)
        :name (name n)
        :object target)))

Removes an observer from the default notification center.

Releases the handler

(defn remove-observer
  [handler]
  ($ default-center :removeObserver handler)
  ($ handler :autorelease))

Post a notification to an object

(defn post-notification
  ([target n] (post-notification target n nil))
  ([target n info]
     ($ default-center
        :postNotificationName (name n)
        :object target
        :userInfo (nsdictionary info))))
(def constraint-regex #"C:(\w*)\.(\w*)(=|<=|>=)(\w*)\.(\w*) ?(-?\w*\.?\w*) ?(-?\w*\.?\w*)")
(defn map-invert [m] (reduce (fn [m [k v]] (assoc m v k)) {} m))
(def layout-constraints
  {:<=      -1
   :=        0
   :>=       1
   :left     1
   :right    2
   :top      3
   :bottom   4
   :leading  5
   :trailing 6
   :width    7
   :height   8
   :centerx  9
   :centery  10
   :baseline 11
   :nil 0})
(def rlayout-constraints (map-invert layout-constraints))
(defn not-found [c]
  (throw (Exception. (str "Constraint not found " c))))
(defn resolve-constraint [c]
  (if (number? c)
    (if (some #{c} (vals layout-constraints)) c (not-found c))
    (if-let [c (layout-constraints (keyword (name c)))] c (not-found c))))
(defn parse-constraint [^String c]
  (if-not (re-find #"^C:" c)
    c
    (if-let [[f1 p1 e f2 p2 m c] (next (re-find constraint-regex c))]
      [(str f1 "-" p1) f1 (resolve-constraint p1) (resolve-constraint e) f2
       (resolve-constraint p2) (if-not (empty? m) (read-string m) 1.0)
       (if-not (empty? c) (read-string c) 0.0)]
      (throw (Exception. (str "Invalid custom constraint: '" c "'.
Use format: C:{name}.[left|right|top|bottom|leading|trailing|width|height|centerx|centery|baseline][=|<=|>=]{name}.[left|right|top|bottom|leading|trailing|width|height|centerx|centery|baseline][=|<=|>=] multiplier? offset?"))))))

Creates NSLayoutConstraints from the constraints definitions

(defn autolayout
  [ui views c]
  (if (string? c)
    (let [c ($ ($ NSLayoutConstraint) :constraintsWithVisualFormat c
               :options 0 :metrics 0
               :views views)]
      ($ ui :addConstraints c)
      c)
    (let [[_ a b c d e f g] c]
      (let [c ($ ($ NSLayoutConstraint)
                 :constraintWithItem ($ views :objectForKey a)
                 :attribute b
                 :relatedBy c
                 :toItem (if (= d "nil") nil ($ views :objectForKey d))
                 :attribute e
                 :multiplier f
                 :constant g)]
        ($ ui :addConstraint c)
        c))))

Sets a property using objc selectors.

Accepts multiple args selectors: :setTitle:forState ["Hello" 0]

(defn set-property
  [view [k v]]
  (let [[view k]
        (if-not (vector? k) [view k]
                (loop [view view [f & other] k]
                  (if-not other
                    [view f]
                    (recur ((sel (name f)) view) other))))]
    (when-not (#{:parent-constraints :constraints :events :gestures} k)
      (if (and (vector? v) (or (empty? v) (re-find #":" (name k))))
        (apply (partial (sel (name k)) view) v)
        ((sel (name k)) view v)))))

Creates a scope for a view

(defn create-scope
  ([] (create-scope {}))
  ([m] (atom (assoc m
               :observers (atom [])
               :state (atom {})
               :retains (atom [])))))
(defn assoc-noreplace [a k v]
  (loop [i 1]
    (let [kk (keyword (str (name k) (if (= 1 i)  i)))]
      (if (@a kk)
        (recur (inc i))
        (swap! a assoc kk v)))))
(defn get-children [children]
  (if (and (= 1 (count children))
           (not (vector? (first children))))
    (first children)
    children))
(defn collect-constraints [[type tag props & children]]
  (into (:constraints props)
        (loop [c [] [f & o] (get-children children)]
          (if f
            (recur (if-let [p (:parent-constraints (nth f 2))]
                     (conj c p)
                     c) o)
            (flatten c)))))

Instantiates a ui from clojure data.

(defn create-ui
  ([v] (create-ui (create-scope) v))
  ([scope [clazz tag props & children :as node]]
     (let [view (if (keyword? clazz) ($ (objc-class (symbol (name clazz))) :new)
                    (do ($ clazz :retain) clazz))
           views (if-let [views (:views @scope)]
                   views
                   (let [views ($ ($ NSMutableDictionary) :new)]
                     (swap! scope assoc :views views)
                      views))]
       ($ views :setValue view :forKey (name tag))
       (swap! scope assoc tag view)
       (swap! (:retains @scope) conj view)
        (doseq [p (if (map? props) props (partition 2 props))]
          (set-property view p))
        (doseq [c (get-children children)]
          (let [s (create-ui scope c)]
            ($ s :setTranslatesAutoresizingMaskIntoConstraints false)
            ($ view :addSubview s)))
        (let [allc (map parse-constraint (collect-constraints node))]
          (when-not (empty? allc)
            (let [rscope (map-invert @scope)]
              (doseq [c allc]
                (if (string? c)
                  (let [l (autolayout view views c)]
                    (when-let [cc ($ l :count)] ;; make it safe for the jvm
                      (doseq [n (range cc)]
                        (let [i ($ l :objectAtIndex n)
                              item1 (rscope ($ i :firstItem))
                              attr1 (rlayout-constraints ($ i :firstAttribute))]
                          (assoc-noreplace scope (str (name item1) "-" (name attr1)) i)
                          (when-let [sec ($ i :secondItem)]
                            (let [item2 (rscope sec)
                                  attr2 (rlayout-constraints ($ i :secondAttribute))]
                              (assoc-noreplace scope (str (name item2) "-" (name attr2)) i)))))))
                  (assoc-noreplace scope (first c) (autolayout view views c)))))))
        (doseq [[k v] (let [g (:gestures props)]
                        (if (map? g) g (partition 2 g)))]
          (let [handler (if (map? v) (:handler v) v)
                selector (sel "invoke")
                handler #(handler @scope)
                g ($ ($ (objc-class (name k)) :alloc)
                     :initWithTarget handler :action selector)]
            ($ handler :retain)
            (swap! (:retains @scope) conj handler)
            (when (map? v)
              (doseq [p (dissoc v :handler)]
                (set-property g p)))
            ($ view :addGestureRecognizer g)))
        (doseq [[k v] (let [g (:events props)]
                        (if (map? g) g (partition 2 g)))]
          (let [handler #(v (assoc @scope :event %))
                method "invokeWithId:"]
            (if (keyword? k)
              (let [kname (name k)]
                (swap! (:observers @scope) conj handler)
                (add-observer view handler method kname))
              (do
                ($ handler :retain)
                (swap! (:retains @scope) conj handler)
                ($ view :addTarget handler :action (sel method)
                   :forControlEvents k)))))
        view)))

Gets the key window

(defn key-window
  []
  (-> ($ UIApplication)
      ($ :sharedApplication)
      ($ :keyWindow)))

Gets the top controller

(defn top-controller
  []
  ($ (key-window) :rootViewController))

Gets the top view

(defn top-view
  []
  ($ (top-controller) :view))

Setups global observers for UIKeyboardWillShowNotification and UIKeyboardWillHideNotification

(defn setup-keyboard
  [keyboard-will-show keyboard-will-hide]
  (add-observer nil keyboard-will-show :UIKeyboardWillShowNotification)
  (add-observer nil keyboard-will-hide :UIKeyboardWillHideNotification))

Deallocs everything in a uikit scope

(defn dealloc
  [scope]
  (doseq [v @(:retains @scope)]
    (post-notification v :dealloc)
    ($ v :release))
  (doseq [v @(:observers @scope)]
    (remove-observer v))
  ($ (:views @scope) :release)
  (reset! scope nil))

uikit internal controller implementation

(defnstype UIKitController UIViewController
  ([^:id self :initWith ^:id [view s]]
     (doto ($$ self :init)
       ($ :setView ($ view :retain))
       (objc-set! :scope s)
       (#(post-notification ($ % :view) :init))))
  ([^:id self :scope]
     @(objc-get self :scope))
  ([self :shouldAutorotate]
     (:shouldAutorotate (objc-get self :scope)))
  ([self :viewDidLoad]
     (post-notification ($ self :view) :viewDidLoad)
     ($$ self :viewDidLoad))
  ([self :didReceiveMemoryWarning]
     (post-notification ($ self :view) :didReceiveMemoryWarning)
     ($$ self :didReceiveMemoryWarning))
  ([self :viewDidLayoutSubviews]
     (post-notification ($ self :view) :viewDidLayoutSubviews)
     ($$ self :viewDidLayoutSubviews))
  ([self :viewWillLayoutSubviews]
     (post-notification ($ self :view) :viewWillLayoutSubviews)
     ($$ self :viewWillLayoutSubviews))
  ([self :viewDidAppear animated]
     (post-notification ($ self :view) :viewDidAppear)
     ($$ self :viewDidAppear animated))
  ([self :viewDidDisappear animated]
     (post-notification ($ self :view) :viewDidDisappear)
     ($$ self :viewDidDisappear animated))
  ([self :viewWillAppear animated]
     (post-notification ($ self :view) :viewWillAppear)
     ($$ self :viewWillAppear animated))
  ([self :viewWillDisappear animated]
     (post-notification ($ self :view) :viewWillDisappear)
     ($$ self :viewWillDisappear animated))
  ([self :dealloc]
     (dealloc (objc-get self :scope))
     ($ ($ self :view) :release)
     ($$ self :dealloc)))

Creates a uikit controller with a title and a view data

(defn controller
  ([title view] (controller title view {}))
  ([title view init]
     (let [scope (create-scope init)
           view (create-ui scope view)]
       (doto ($ ($ ($ UIKitController) :alloc)
                :initWith [view scope])
         ($ :setTitle title)
         ($ :setView view)
         ($ :autorelease)))))

Pushes a controller into the top navigation controller

(defn nav-push
  ([controller] (nav-push controller false))
  ([controller animated] ($ (top-controller) :pushViewController controller :animated animated)))

Pops a controller from the current navigation controller

(defn nav-pop
  ([] (nav-pop true))
  ([animated]
     ($ (top-controller) :popViewControllerAnimated animated)))

Gets the top controller from the current navigation controller

(defn nav-top-controller
  ([] (nav-top-controller (top-controller)))
  ([nav] ($ nav :visibleViewController)))

Creates and shows a simple UIAlertView

(defn alert!
  ([title msg] (alert! title msg "Cancel" nil))
  ([title msg cancel] (alert! title msg cancel nil))
  ([title msg cancel delegate]
     (-> ($ UIAlertView)
         ($ :alloc)
         ($ :initWithTitle title
            :message msg
            :delegate delegate
            :cancelButtonTitle cancel
            :otherButtonTitles nil)
         ($ :autorelease)
         ($ :show))))

Creates a UIButton with a type

(defn button
  [type] ($ ($ UIButton) :buttonWithType type))

Mutates ui

(ui! scope {:field:setMethod val1 :other:setSomethingElse val2})

(defmacro ui!
  [scope pairs]
  (let [code
        (for [[k v] pairs]
          (let [[_ field method] (re-matches #"(\w*):(.*)" (name k))]
            (when (and field method)
              (let [kfield (keyword field)
                    view `(~scope ~kfield)
                    kmethod (keyword method)]
                (if (= :nop v)
                  `((sel ~method) ~view)
                  `(set-property ~view [~kmethod ~v]))))))]
    `(clojure.lang.RT/dispatchInMainSync (fn [] ~@code))))