diff --git a/404.html b/404.html new file mode 100644 index 0000000..bd7cac0 --- /dev/null +++ b/404.html @@ -0,0 +1,37 @@ + + +
+ + + + + +Estimated time
1/4 day
Communicating with a REST API relies on multiple concepts that we'll cover briefly below:
Serializable
in Java. This type is helpful if we want to convert an object into an out of a JSON string.async
and await
: these keywords are used to call an asynchronous function using a synchronous coding fashion. This means that callbacks are needed no more!This PW relies on the excellent tutorial from hackingwithswift. It guides you on how to fetch JSON data from iTunes's API. Please find and excerpt of the response body below.
{
+ "resultCount":50,
+ "results":[
+ {
+ "wrapperType":"track",
+ "kind":"song",
+ "artistId":159260351,
+ "collectionId":1440913923,
+ "trackId":1440914010,
+ "artistName":"Taylor Swift",
+ "collectionName":"Taylor Swift (Bonus Track Version)",
+ "trackName":"Our Song",
+ "collectionCensoredName":"Taylor Swift (Bonus Track Version)",
+ }
+ ]
+}
+
K?Oe(h,S,$,!0,!1,Q):y(p,_,k,S,$,z,N,F,Q)},He=(h,p,_,k,S,$,z,N,F)=>{let P=0;const K=p.length;let Q=h.length-1,J=K-1;for(;P<=Q&&P<=J;){const re=h[P],ae=p[P]=F?Rt(p[P]):ot(p[P]);if(Jt(re,ae))b(re,ae,_,null,S,$,z,N,F);else break;P++}for(;P<=Q&&P<=J;){const re=h[Q],ae=p[J]=F?Rt(p[J]):ot(p[J]);if(Jt(re,ae))b(re,ae,_,null,S,$,z,N,F);else break;Q--,J--}if(P>Q){if(P<=J){const re=J+1,ae=reJ)for(;P<=Q;)Be(h[P],S,$,!0),P++;else{const re=P,ae=P,ve=new Map;for(P=ae;P<=J;P++){const Ge=p[P]=F?Rt(p[P]):ot(p[P]);Ge.key!=null&&ve.set(Ge.key,P)}let Ee,Ie=0;const nt=J-ae+1;let ln=!1,ls=0;const En=new Array(nt);for(P=0;P =nt){Be(Ge,S,$,!0);continue}let ft;if(Ge.key!=null)ft=ve.get(Ge.key);else for(Ee=ae;Ee<=J;Ee++)if(En[Ee-ae]===0&&Jt(Ge,p[Ee])){ft=Ee;break}ft===void 0?Be(Ge,S,$,!0):(En[ft-ae]=P+1,ft>=ls?ls=ft:ln=!0,b(Ge,p[ft],_,null,S,$,z,N,F),Ie++)}const as=ln?Yc(En):un;for(Ee=as.length-1,P=nt-1;P>=0;P--){const Ge=ae+P,ft=p[Ge],cs=Ge+1 {const{el:$,type:z,transition:N,children:F,shapeFlag:P}=h;if(P&6){We(h.component.subTree,p,_,k);return}if(P&128){h.suspense.move(p,_,k);return}if(P&64){z.move(h,p,_,q);return}if(z===_e){r($,p,_);for(let Q=0;Q N.enter($),S);else{const{leave:Q,delayLeave:J,afterLeave:re}=N,ae=()=>r($,p,_),ve=()=>{Q($,()=>{ae(),re&&re()})};J?J($,ae,ve):ve()}else r($,p,_)},Be=(h,p,_,k=!1,S=!1)=>{const{type:$,props:z,ref:N,children:F,dynamicChildren:P,shapeFlag:K,patchFlag:Q,dirs:J}=h;if(N!=null&&Ar(N,null,_,h,!0),K&256){p.ctx.deactivate(h);return}const re=K&1&&J,ae=!pn(h);let ve;if(ae&&(ve=z&&z.onVnodeBeforeUnmount)&&Qe(ve,p,h),K&6)ut(h.component,_,k);else{if(K&128){h.suspense.unmount(_,k);return}re&&dt(h,null,p,"beforeUnmount"),K&64?h.type.remove(h,p,_,S,q,k):P&&($!==_e||Q>0&&Q&64)?Oe(P,p,_,!1,!0):($===_e&&Q&384||!S&&K&16)&&Oe(F,p,_),k&&Ct(h)}(ae&&(ve=z&&z.onVnodeUnmounted)||re)&&Ve(()=>{ve&&Qe(ve,p,h),re&&dt(h,null,p,"unmounted")},_)},Ct=h=>{const{type:p,el:_,anchor:k,transition:S}=h;if(p===_e){St(_,k);return}if(p===In){C(h);return}const $=()=>{o(_),S&&!S.persisted&&S.afterLeave&&S.afterLeave()};if(h.shapeFlag&1&&S&&!S.persisted){const{leave:z,delayLeave:N}=S,F=()=>z(_,$);N?N(h.el,$,F):F()}else $()},St=(h,p)=>{let _;for(;h!==p;)_=d(h),o(h),h=_;o(p)},ut=(h,p,_)=>{const{bum:k,scope:S,update:$,subTree:z,um:N}=h;k&&Jr(k),S.stop(),$&&($.active=!1,Be(z,h,p,_)),N&&Ve(N,p),Ve(()=>{h.isUnmounted=!0},p),p&&p.pendingBranch&&!p.isUnmounted&&h.asyncDep&&!h.asyncResolved&&h.suspenseId===p.pendingId&&(p.deps--,p.deps===0&&p.resolve())},Oe=(h,p,_,k=!1,S=!1,$=0)=>{for(let z=$;z h.shapeFlag&6?x(h.component.subTree):h.shapeFlag&128?h.suspense.next():d(h.anchor||h.el);let U=!1;const D=(h,p,_)=>{h==null?p._vnode&&Be(p._vnode,null,null,!0):b(p._vnode||null,h,p,null,null,null,_),U||(U=!0,ys(),Sr(),U=!1),p._vnode=h},q={p:b,um:Be,m:We,r:Ct,mt:te,mc:y,pc:V,pbc:L,n:x,o:e};let ue,ge;return t&&([ue,ge]=t(q)),{render:D,hydrate:ue,createApp:Fc(D,ue)}}function no({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function Wt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function ml(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function gl(e,t,n=!1){const r=e.children,o=t.children;if(ee(r)&&ee(o))for(let s=0;s >1,e[n[l]] 0&&(t[r]=n[s-1]),n[s]=r)}}for(s=n.length,i=n[s-1];s-- >0;)n[s]=i,i=t[i];return n}function vl(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:vl(t)}const Jc=e=>e.__isTeleport,_e=Symbol.for("v-fgt"),gn=Symbol.for("v-txt"),Je=Symbol.for("v-cmt"),In=Symbol.for("v-stc"),$n=[];let st=null;function j(e=!1){$n.push(st=e?null:[])}function Xc(){$n.pop(),st=$n[$n.length-1]||null}let zn=1;function Ps(e){zn+=e}function yl(e){return e.dynamicChildren=zn>0?st||un:null,Xc(),zn>0&&st&&st.push(e),e}function Z(e,t,n,r,o,s){return yl(ne(e,t,n,r,o,s,!0))}function xe(e,t,n,r,o){return yl(se(e,t,n,r,o,!0))}function Tr(e){return e?e.__v_isVNode===!0:!1}function Jt(e,t){return e.type===t.type&&e.key===t.key}const _l=({key:e})=>e??null,_r=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?Ae(e)||je(e)||le(e)?{i:Te,r:e,k:t,f:!!n}:e:null);function ne(e,t=null,n=null,r=0,o=null,s=e===_e?0:1,i=!1,l=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&_l(t),ref:t&&_r(t),scopeId:Qi,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:r,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:Te};return l?(qo(a,n),s&128&&e.normalize(a)):n&&(a.shapeFlag|=Ae(n)?8:16),zn>0&&!i&&st&&(a.patchFlag>0||s&6)&&a.patchFlag!==32&&st.push(a),a}const se=Qc;function Qc(e,t=null,n=null,r=0,o=null,s=!1){if((!e||e===uc)&&(e=Je),Tr(e)){const l=Bt(e,t,!0);return n&&qo(l,n),zn>0&&!s&&st&&(l.shapeFlag&6?st[st.indexOf(e)]=l:st.push(l)),l.patchFlag|=-2,l}if(cu(e)&&(e=e.__vccOpts),t){t=Zc(t);let{class:l,style:a}=t;l&&!Ae(l)&&(t.class=Ke(l)),we(a)&&(Wi(a)&&!ee(a)&&(a=Pe({},a)),t.style=Jn(a))}const i=Ae(e)?1:dc(e)?128:Jc(e)?64:we(e)?4:le(e)?2:0;return ne(e,t,n,r,o,i,s,!0)}function Zc(e){return e?Wi(e)||Object.getPrototypeOf(e)===cl?Pe({},e):e:null}function Bt(e,t,n=!1){const{props:r,ref:o,patchFlag:s,children:i}=e,l=t?So(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&_l(l),ref:t&&t.ref?n&&o?ee(o)?o.concat(_r(t)):[o,_r(t)]:_r(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:i,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==_e?s===-1?16:s|16:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Bt(e.ssContent),ssFallback:e.ssFallback&&Bt(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function mt(e=" ",t=0){return se(gn,null,e,t)}function eu(e,t){const n=se(In,null,e);return n.staticCount=t,n}function ke(e="",t=!1){return t?(j(),xe(Je,null,e)):se(Je,null,e)}function ot(e){return e==null||typeof e=="boolean"?se(Je):ee(e)?se(_e,null,e.slice()):typeof e=="object"?Rt(e):se(gn,null,String(e))}function Rt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Bt(e)}function qo(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(ee(t))n=16;else if(typeof t=="object")if(r&65){const o=t.default;o&&(o._c&&(o._d=!1),qo(e,o()),o._c&&(o._d=!0));return}else{n=32;const o=t._;o?o===3&&Te&&(Te.slots._===1?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=Te}else le(t)?(t={default:t,_ctx:Te},n=32):(t=String(t),r&64?(n=16,t=[mt(t)]):n=8);e.children=t,e.shapeFlag|=n}function So(...e){const t={};for(let n=0;n $e||Te;let Pr,xo;{const e=Ai(),t=(n,r)=>{let o;return(o=e[n])||(o=e[n]=[]),o.push(r),s=>{o.length>1?o.forEach(i=>i(s)):o[0](s)}};Pr=t("__VUE_INSTANCE_SETTERS__",n=>$e=n),xo=t("__VUE_SSR_SETTERS__",n=>tr=n)}const er=e=>{const t=$e;return Pr(e),e.scope.on(),()=>{e.scope.off(),Pr(t)}},Rs=()=>{$e&&$e.scope.off(),Pr(null)};function bl(e){return e.vnode.shapeFlag&4}let tr=!1;function ou(e,t=!1){t&&xo(t);const{props:n,children:r}=e.vnode,o=bl(e);Bc(e,n,o,t),jc(e,r);const s=o?su(e,t):void 0;return t&&xo(!1),s}function su(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Rc);const{setup:r}=n;if(r){const o=e.setupContext=r.length>1?lu(e):null,s=er(e);zt();const i=Nt(r,e,0,[e.props,o]);if(jt(),s(),Si(i)){if(i.then(Rs,Rs),t)return i.then(l=>{Os(e,l,t)}).catch(l=>{Qn(l,e,0)});e.asyncDep=i}else Os(e,i,t)}else wl(e,t)}function Os(e,t,n){le(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:we(t)&&(e.setupState=qi(t)),wl(e,n)}let Is;function wl(e,t,n){const r=e.type;if(!e.render){if(!t&&Is&&!r.render){const o=r.template||Vo(e).template;if(o){const{isCustomElement:s,compilerOptions:i}=e.appContext.config,{delimiters:l,compilerOptions:a}=r,c=Pe(Pe({isCustomElement:s,delimiters:l},i),a);r.render=Is(o,c)}}e.render=r.render||Ze}{const o=er(e);zt();try{Oc(e)}finally{jt(),o()}}}const iu={get(e,t){return qe(e,"get",""),e[t]}};function lu(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,iu),slots:e.slots,emit:e.emit,expose:t}}function Wr(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(qi(Ka(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Rn)return Rn[n](e)},has(t,n){return n in t||n in Rn}}))}function au(e,t=!0){return le(e)?e.displayName||e.name:e.name||t&&e.__name}function cu(e){return le(e)&&"__vccOpts"in e}const I=(e,t)=>qa(e,t,tr);function oe(e,t,n){const r=arguments.length;return r===2?we(t)&&!ee(t)?Tr(t)?se(e,null,[t]):se(e,t):se(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&Tr(n)&&(n=[n]),se(e,t,n))}const uu="3.4.22";/** +* @vue/runtime-dom v3.4.22 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const fu="http://www.w3.org/2000/svg",du="http://www.w3.org/1998/Math/MathML",Ot=typeof document<"u"?document:null,$s=Ot&&Ot.createElement("template"),hu={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t==="svg"?Ot.createElementNS(fu,e):t==="mathml"?Ot.createElementNS(du,e):Ot.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>Ot.createTextNode(e),createComment:e=>Ot.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ot.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const i=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{$s.innerHTML=r==="svg"?``:r==="mathml"?``:e;const l=$s.content;if(r==="svg"||r==="mathml"){const a=l.firstChild;for(;a.firstChild;)l.appendChild(a.firstChild);l.removeChild(a)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},kt="transition",Cn="animation",jn=Symbol("_vtc"),bn=(e,{slots:t})=>oe(_c,pu(e),t);bn.displayName="Transition";const El={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};bn.props=Pe({},nl,El);const Vt=(e,t=[])=>{ee(e)?e.forEach(n=>n(...t)):e&&e(...t)},Ns=e=>e?ee(e)?e.some(t=>t.length>1):e.length>1:!1;function pu(e){const t={};for(const w in e)w in El||(t[w]=e[w]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:a=s,appearActiveClass:c=i,appearToClass:u=l,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:m=`${n}-leave-to`}=e,g=mu(o),b=g&&g[0],E=g&&g[1],{onBeforeEnter:A,onEnter:T,onEnterCancelled:v,onLeave:C,onLeaveCancelled:B,onBeforeAppear:O=A,onAppear:H=T,onAppearCancelled:y=v}=t,G=(w,M,te)=>{Kt(w,M?u:l),Kt(w,M?c:i),te&&te()},L=(w,M)=>{w._isLeaving=!1,Kt(w,f),Kt(w,m),Kt(w,d),M&&M()},W=w=>(M,te)=>{const ie=w?H:T,R=()=>G(M,w,te);Vt(ie,[M,R]),Ms(()=>{Kt(M,w?a:s),At(M,w?u:l),Ns(ie)||Hs(M,r,b,R)})};return Pe(t,{onBeforeEnter(w){Vt(A,[w]),At(w,s),At(w,i)},onBeforeAppear(w){Vt(O,[w]),At(w,a),At(w,c)},onEnter:W(!1),onAppear:W(!0),onLeave(w,M){w._isLeaving=!0;const te=()=>L(w,M);At(w,f),yu(),At(w,d),Ms(()=>{w._isLeaving&&(Kt(w,f),At(w,m),Ns(C)||Hs(w,r,E,te))}),Vt(C,[w,te])},onEnterCancelled(w){G(w,!1),Vt(v,[w])},onAppearCancelled(w){G(w,!0),Vt(y,[w])},onLeaveCancelled(w){L(w),Vt(B,[w])}})}function mu(e){if(e==null)return null;if(we(e))return[ro(e.enter),ro(e.leave)];{const t=ro(e);return[t,t]}}function ro(e){return ga(e)}function At(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[jn]||(e[jn]=new Set)).add(t)}function Kt(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[jn];n&&(n.delete(t),n.size||(e[jn]=void 0))}function Ms(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let gu=0;function Hs(e,t,n,r){const o=e._endId=++gu,s=()=>{o===e._endId&&r()};if(n)return setTimeout(s,n);const{type:i,timeout:l,propCount:a}=vu(e,t);if(!i)return r();const c=i+"end";let u=0;const f=()=>{e.removeEventListener(c,d),s()},d=m=>{m.target===e&&++u>=a&&f()};setTimeout(()=>{u(n[g]||"").split(", "),o=r(`${kt}Delay`),s=r(`${kt}Duration`),i=Fs(o,s),l=r(`${Cn}Delay`),a=r(`${Cn}Duration`),c=Fs(l,a);let u=null,f=0,d=0;t===kt?i>0&&(u=kt,f=i,d=s.length):t===Cn?c>0&&(u=Cn,f=c,d=a.length):(f=Math.max(i,c),u=f>0?i>c?kt:Cn:null,d=u?u===kt?s.length:a.length:0);const m=u===kt&&/\b(transform|all)(,|$)/.test(r(`${kt}Property`).toString());return{type:u,timeout:f,propCount:d,hasTransform:m}}function Fs(e,t){for(;e.length Bs(n)+Bs(e[r])))}function Bs(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function yu(){return document.body.offsetHeight}function _u(e,t,n){const r=e[jn];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Rr=Symbol("_vod"),Cl=Symbol("_vsh"),Or={beforeMount(e,{value:t},{transition:n}){e[Rr]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):Sn(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),Sn(e,!0),r.enter(e)):r.leave(e,()=>{Sn(e,!1)}):Sn(e,t))},beforeUnmount(e,{value:t}){Sn(e,t)}};function Sn(e,t){e.style.display=t?e[Rr]:"none",e[Cl]=!t}const bu=Symbol(""),wu=/(^|;)\s*display\s*:/;function Eu(e,t,n){const r=e.style,o=Ae(n);let s=!1;if(n&&!o){if(t)if(Ae(t))for(const i of t.split(";")){const l=i.slice(0,i.indexOf(":")).trim();n[l]==null&&br(r,l,"")}else for(const i in t)n[i]==null&&br(r,i,"");for(const i in n)i==="display"&&(s=!0),br(r,i,n[i])}else if(o){if(t!==n){const i=r[bu];i&&(n+=";"+i),r.cssText=n,s=wu.test(n)}}else t&&e.removeAttribute("style");Rr in e&&(e[Rr]=s?r.display:"",e[Cl]&&(r.display="none"))}const Ds=/\s*!important$/;function br(e,t,n){if(ee(n))n.forEach(r=>br(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Cu(e,t);Ds.test(n)?e.setProperty(rn(r),n.replace(Ds,""),"important"):e[r]=n}}const zs=["Webkit","Moz","ms"],oo={};function Cu(e,t){const n=oo[t];if(n)return n;let r=tt(t);if(r!=="filter"&&r in e)return oo[t]=r;r=Yn(r);for(let o=0;o so||(Pu.then(()=>so=0),so=Date.now());function Ou(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;et(Iu(r,n.value),t,5,[r])};return n.value=e,n.attached=Ru(),n}function Iu(e,t){if(ee(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Vs=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,$u=(e,t,n,r,o,s,i,l,a)=>{const c=o==="svg";t==="class"?_u(e,r,c):t==="style"?Eu(e,n,r):Gn(t)?Oo(t)||Au(e,t,n,r,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Nu(e,t,r,c))?xu(e,t,r,s,i,l,a):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Su(e,t,r,c))};function Nu(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t in e&&Vs(t)&&le(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const o=e.tagName;if(o==="IMG"||o==="VIDEO"||o==="CANVAS"||o==="SOURCE")return!1}return Vs(t)&&Ae(n)?!1:t in e}const Mu={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Hu=(e,t)=>{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=o=>{if(!("key"in o))return;const s=rn(o.key);if(t.some(i=>i===s||Mu[i]===s))return e(o)})},Fu=Pe({patchProp:$u},hu);let io,Ks=!1;function Bu(){return io=Ks?io:qc(Fu),Ks=!0,io}const Du=(...e)=>{const t=Bu().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=ju(r);if(o)return n(o,!0,zu(o))},t};function zu(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function ju(e){return Ae(e)?document.querySelector(e):e}var Uu=["link","meta","script","style","noscript","template"],Wu=["title","base"],Vu=([e,t,n])=>Wu.includes(e)?e:Uu.includes(e)?e==="meta"&&t.name?`${e}.${t.name}`:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,Object.entries(t).map(([r,o])=>typeof o=="boolean"?o?[r,""]:null:[r,o]).filter(r=>r!=null).sort(([r],[o])=>r.localeCompare(o)),n]):null,Ku=e=>{const t=new Set,n=[];return e.forEach(r=>{const o=Vu(r);o&&!t.has(o)&&(t.add(o),n.push(r))}),n},nr=e=>/^(https?:)?\/\//.test(e),Sl=e=>/^[a-z][a-z0-9+.-]*:/.test(e),Go=e=>Object.prototype.toString.call(e)==="[object Object]",qu=e=>{const[t,...n]=e.split(/(\?|#)/);if(!t||t.endsWith("/"))return e;let r=t.replace(/(^|\/)README.md$/i,"$1index.html");return r.endsWith(".md")?r=r.substring(0,r.length-3)+".html":r.endsWith(".html")||(r=r+".html"),r.endsWith("/index.html")&&(r=r.substring(0,r.length-10)),r+n.join("")},xl=e=>e[e.length-1]==="/"?e.slice(0,-1):e,Ll=e=>e[0]==="/"?e.slice(1):e,kl=(e,t)=>{const n=Object.keys(e).sort((r,o)=>{const s=o.split("/").length-r.split("/").length;return s!==0?s:o.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"},it=e=>typeof e=="string";const Gu="modulepreload",Yu=function(e){return"/ios-training/"+e},qs={},rt=function(t,n,r){let o=Promise.resolve();if(n&&n.length>0){const s=document.getElementsByTagName("link"),i=document.querySelector("meta[property=csp-nonce]"),l=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute("nonce"));o=Promise.all(n.map(a=>{if(a=Yu(a),a in qs)return;qs[a]=!0;const c=a.endsWith(".css"),u=c?'[rel="stylesheet"]':"";if(!!r)for(let m=s.length-1;m>=0;m--){const g=s[m];if(g.href===a&&(!c||g.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${a}"]${u}`))return;const d=document.createElement("link");if(d.rel=c?"stylesheet":Gu,c||(d.as="script",d.crossOrigin=""),d.href=a,l&&d.setAttribute("nonce",l),document.head.appendChild(d),c)return new Promise((m,g)=>{d.addEventListener("load",m),d.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${a}`)))})}))}return o.then(()=>t()).catch(s=>{const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=s,window.dispatchEvent(i),!i.defaultPrevented)throw s})},Ju=JSON.parse("{}"),Xu=Object.fromEntries([["/",{loader:()=>rt(()=>import("./index.html-C39pB5w2.js"),[]),meta:{title:"Welcome"}}],["/api-communication/",{loader:()=>rt(()=>import("./index.html-Cv2fRjYf.js"),[]),meta:{title:"Communicate with a REST API"}}],["/mini-project/",{loader:()=>rt(()=>import("./index.html-z4YYCP43.js"),[]),meta:{title:"Mini project"}}],["/persist-data/",{loader:()=>rt(()=>import("./index.html-ChAAQVq_.js"),[]),meta:{title:"Locally persisting data"}}],["/presentation/",{loader:()=>rt(()=>import("./index.html-DOlN3MeL.js"),[]),meta:{title:"Presentation"}}],["/swift-part1/",{loader:()=>rt(()=>import("./index.html-B8dggsdP.js"),[]),meta:{title:"Swift (part 1)"}}],["/swift-part2/",{loader:()=>rt(()=>import("./index.html-5n7MuywO.js"),[]),meta:{title:"Swift (part 2)"}}],["/to-go-further/",{loader:()=>rt(()=>import("./index.html-CfxHtkWF.js"),[]),meta:{title:"Going further"}}],["/ui-development/",{loader:()=>rt(()=>import("./index.html-DKuyFSlS.js"),[]),meta:{title:"UI development"}}],["/404.html",{loader:()=>rt(()=>import("./404.html-9pEcDQrh.js"),[]),meta:{title:""}}]]);/*! + * vue-router v4.3.0 + * (c) 2024 Eduardo San Martin Morote + * @license MIT + */const cn=typeof document<"u";function Qu(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const me=Object.assign;function lo(e,t){const n={};for(const r in t){const o=t[r];n[r]=lt(o)?o.map(e):e(o)}return n}const Nn=()=>{},lt=Array.isArray,Al=/#/g,Zu=/&/g,ef=/\//g,tf=/=/g,nf=/\?/g,Tl=/\+/g,rf=/%5B/g,of=/%5D/g,Pl=/%5E/g,sf=/%60/g,Rl=/%7B/g,lf=/%7C/g,Ol=/%7D/g,af=/%20/g;function Yo(e){return encodeURI(""+e).replace(lf,"|").replace(rf,"[").replace(of,"]")}function cf(e){return Yo(e).replace(Rl,"{").replace(Ol,"}").replace(Pl,"^")}function Lo(e){return Yo(e).replace(Tl,"%2B").replace(af,"+").replace(Al,"%23").replace(Zu,"%26").replace(sf,"`").replace(Rl,"{").replace(Ol,"}").replace(Pl,"^")}function uf(e){return Lo(e).replace(tf,"%3D")}function ff(e){return Yo(e).replace(Al,"%23").replace(nf,"%3F")}function df(e){return e==null?"":ff(e).replace(ef,"%2F")}function Un(e){try{return decodeURIComponent(""+e)}catch{}return""+e}const hf=/\/$/,pf=e=>e.replace(hf,"");function ao(e,t,n="/"){let r,o={},s="",i="";const l=t.indexOf("#");let a=t.indexOf("?");return l=0&&(a=-1),a>-1&&(r=t.slice(0,a),s=t.slice(a+1,l>-1?l:t.length),o=e(s)),l>-1&&(r=r||t.slice(0,l),i=t.slice(l,t.length)),r=yf(r??t,n),{fullPath:r+(s&&"?")+s+i,path:r,query:o,hash:Un(i)}}function mf(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Gs(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function gf(e,t,n){const r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&vn(t.matched[r],n.matched[o])&&Il(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function vn(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Il(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!vf(e[n],t[n]))return!1;return!0}function vf(e,t){return lt(e)?Ys(e,t):lt(t)?Ys(t,e):e===t}function Ys(e,t){return lt(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function yf(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/"),o=r[r.length-1];(o===".."||o===".")&&r.push("");let s=n.length-1,i,l;for(i=0;i 1&&s--;else break;return n.slice(0,s).join("/")+"/"+r.slice(i).join("/")}var Wn;(function(e){e.pop="pop",e.push="push"})(Wn||(Wn={}));var Mn;(function(e){e.back="back",e.forward="forward",e.unknown=""})(Mn||(Mn={}));function _f(e){if(!e)if(cn){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),pf(e)}const bf=/^[^#]+#/;function wf(e,t){return e.replace(bf,"#")+t}function Ef(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const Vr=()=>({left:window.scrollX,top:window.scrollY});function Cf(e){let t;if("el"in e){const n=e.el,r=typeof n=="string"&&n.startsWith("#"),o=typeof n=="string"?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=Ef(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Js(e,t){return(history.state?history.state.position-t:-1)+e}const ko=new Map;function Sf(e,t){ko.set(e,t)}function xf(e){const t=ko.get(e);return ko.delete(e),t}let Lf=()=>location.protocol+"//"+location.host;function $l(e,t){const{pathname:n,search:r,hash:o}=t,s=e.indexOf("#");if(s>-1){let l=o.includes(e.slice(s))?e.slice(s).length:1,a=o.slice(l);return a[0]!=="/"&&(a="/"+a),Gs(a,"")}return Gs(n,e)+r+o}function kf(e,t,n,r){let o=[],s=[],i=null;const l=({state:d})=>{const m=$l(e,location),g=n.value,b=t.value;let E=0;if(d){if(n.value=m,t.value=d,i&&i===g){i=null;return}E=b?d.position-b.position:0}else r(m);o.forEach(A=>{A(n.value,g,{delta:E,type:Wn.pop,direction:E?E>0?Mn.forward:Mn.back:Mn.unknown})})};function a(){i=n.value}function c(d){o.push(d);const m=()=>{const g=o.indexOf(d);g>-1&&o.splice(g,1)};return s.push(m),m}function u(){const{history:d}=window;d.state&&d.replaceState(me({},d.state,{scroll:Vr()}),"")}function f(){for(const d of s)d();s=[],window.removeEventListener("popstate",l),window.removeEventListener("beforeunload",u)}return window.addEventListener("popstate",l),window.addEventListener("beforeunload",u,{passive:!0}),{pauseListeners:a,listen:c,destroy:f}}function Xs(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?Vr():null}}function Af(e){const{history:t,location:n}=window,r={value:$l(e,n)},o={value:t.state};o.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function s(a,c,u){const f=e.indexOf("#"),d=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+a:Lf()+e+a;try{t[u?"replaceState":"pushState"](c,"",d),o.value=c}catch(m){console.error(m),n[u?"replace":"assign"](d)}}function i(a,c){const u=me({},t.state,Xs(o.value.back,a,o.value.forward,!0),c,{position:o.value.position});s(a,u,!0),r.value=a}function l(a,c){const u=me({},o.value,t.state,{forward:a,scroll:Vr()});s(u.current,u,!0);const f=me({},Xs(r.value,a,null),{position:u.position+1},c);s(a,f,!1),r.value=a}return{location:r,state:o,push:l,replace:i}}function Tf(e){e=_f(e);const t=Af(e),n=kf(e,t.state,t.location,t.replace);function r(s,i=!0){i||n.pauseListeners(),history.go(s)}const o=me({location:"",base:e,go:r,createHref:wf.bind(null,e)},t,n);return Object.defineProperty(o,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(o,"state",{enumerable:!0,get:()=>t.state.value}),o}function Pf(e){return typeof e=="string"||e&&typeof e=="object"}function Nl(e){return typeof e=="string"||typeof e=="symbol"}const _t={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},Ml=Symbol("");var Qs;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Qs||(Qs={}));function yn(e,t){return me(new Error,{type:e,[Ml]:!0},t)}function yt(e,t){return e instanceof Error&&Ml in e&&(t==null||!!(e.type&t))}const Zs="[^/]+?",Rf={sensitive:!1,strict:!1,start:!0,end:!0},Of=/[.+*?^${}()[\]/\\]/g;function If(e,t){const n=me({},Rf,t),r=[];let o=n.start?"^":"";const s=[];for(const c of e){const u=c.length?[]:[90];n.strict&&!c.length&&(o+="/");for(let f=0;f t.length?t.length===1&&t[0]===80?1:-1:0}function Nf(e,t){let n=0;const r=e.score,o=t.score;for(;n 0&&t[t.length-1]<0}const Mf={type:0,value:""},Hf=/[a-zA-Z0-9_]/;function Ff(e){if(!e)return[[]];if(e==="/")return[[Mf]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(m){throw new Error(`ERR (${n})/"${c}": ${m}`)}let n=0,r=n;const o=[];let s;function i(){s&&o.push(s),s=[]}let l=0,a,c="",u="";function f(){c&&(n===0?s.push({type:0,value:c}):n===1||n===2||n===3?(s.length>1&&(a==="*"||a==="+")&&t(`A repeatable param (${c}) must be alone in its segment. eg: '/:ids+.`),s.push({type:1,value:c,regexp:u,repeatable:a==="*"||a==="+",optional:a==="*"||a==="?"})):t("Invalid state to consume buffer"),c="")}function d(){c+=a}for(;l {i(T)}:Nn}function i(u){if(Nl(u)){const f=r.get(u);f&&(r.delete(u),n.splice(n.indexOf(f),1),f.children.forEach(i),f.alias.forEach(i))}else{const f=n.indexOf(u);f>-1&&(n.splice(f,1),u.record.name&&r.delete(u.record.name),u.children.forEach(i),u.alias.forEach(i))}}function l(){return n}function a(u){let f=0;for(;f =0&&(u.record.path!==n[f].record.path||!Hl(u,n[f]));)f++;n.splice(f,0,u),u.record.name&&!ni(u)&&r.set(u.record.name,u)}function c(u,f){let d,m={},g,b;if("name"in u&&u.name){if(d=r.get(u.name),!d)throw yn(1,{location:u});b=d.record.name,m=me(ti(f.params,d.keys.filter(T=>!T.optional).concat(d.parent?d.parent.keys.filter(T=>T.optional):[]).map(T=>T.name)),u.params&&ti(u.params,d.keys.map(T=>T.name))),g=d.stringify(m)}else if(u.path!=null)g=u.path,d=n.find(T=>T.re.test(g)),d&&(m=d.parse(g),b=d.record.name);else{if(d=f.name?r.get(f.name):n.find(T=>T.re.test(f.path)),!d)throw yn(1,{location:u,currentLocation:f});b=d.record.name,m=me({},f.params,u.params),g=d.stringify(m)}const E=[];let A=d;for(;A;)E.unshift(A.record),A=A.parent;return{name:b,path:g,params:m,matched:E,meta:Uf(E)}}return e.forEach(u=>s(u)),{addRoute:s,resolve:c,removeRoute:i,getRoutes:l,getRecordMatcher:o}}function ti(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function zf(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:jf(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function jf(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]=typeof n=="object"?n[r]:n;return t}function ni(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Uf(e){return e.reduce((t,n)=>me(t,n.meta),{})}function ri(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function Hl(e,t){return t.children.some(n=>n===e||Hl(e,n))}function Wf(e){const t={};if(e===""||e==="?")return t;const r=(e[0]==="?"?e.slice(1):e).split("&");for(let o=0;o s&&Lo(s)):[r&&Lo(r)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+n,s!=null&&(t+="="+s))})}return t}function Vf(e){const t={};for(const n in e){const r=e[n];r!==void 0&&(t[n]=lt(r)?r.map(o=>o==null?null:""+o):r==null?r:""+r)}return t}const Kf=Symbol(""),si=Symbol(""),Kr=Symbol(""),Jo=Symbol(""),Ao=Symbol("");function xn(){let e=[];function t(r){return e.push(r),()=>{const o=e.indexOf(r);o>-1&&e.splice(o,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function It(e,t,n,r,o,s=i=>i()){const i=r&&(r.enterCallbacks[o]=r.enterCallbacks[o]||[]);return()=>new Promise((l,a)=>{const c=d=>{d===!1?a(yn(4,{from:n,to:t})):d instanceof Error?a(d):Pf(d)?a(yn(2,{from:t,to:d})):(i&&r.enterCallbacks[o]===i&&typeof d=="function"&&i.push(d),l())},u=s(()=>e.call(r&&r.instances[o],t,n,c));let f=Promise.resolve(u);e.length<3&&(f=f.then(c)),f.catch(d=>a(d))})}function co(e,t,n,r,o=s=>s()){const s=[];for(const i of e)for(const l in i.components){let a=i.components[l];if(!(t!=="beforeRouteEnter"&&!i.instances[l]))if(qf(a)){const u=(a.__vccOpts||a)[t];u&&s.push(It(u,n,r,i,l,o))}else{let c=a();s.push(()=>c.then(u=>{if(!u)return Promise.reject(new Error(`Couldn't resolve component "${l}" at "${i.path}"`));const f=Qu(u)?u.default:u;i.components[l]=f;const m=(f.__vccOpts||f)[t];return m&&It(m,n,r,i,l,o)()}))}}return s}function qf(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function ii(e){const t=ze(Kr),n=ze(Jo),r=I(()=>t.resolve(X(e.to))),o=I(()=>{const{matched:a}=r.value,{length:c}=a,u=a[c-1],f=n.matched;if(!u||!f.length)return-1;const d=f.findIndex(vn.bind(null,u));if(d>-1)return d;const m=li(a[c-2]);return c>1&&li(u)===m&&f[f.length-1].path!==m?f.findIndex(vn.bind(null,a[c-2])):d}),s=I(()=>o.value>-1&&Xf(n.params,r.value.params)),i=I(()=>o.value>-1&&o.value===n.matched.length-1&&Il(n.params,r.value.params));function l(a={}){return Jf(a)?t[X(e.replace)?"replace":"push"](X(e.to)).catch(Nn):Promise.resolve()}return{route:r,href:I(()=>r.value.href),isActive:s,isExactActive:i,navigate:l}}const Gf=pe({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:ii,setup(e,{slots:t}){const n=Xn(ii(e)),{options:r}=ze(Kr),o=I(()=>({[ai(e.activeClass,r.linkActiveClass,"router-link-active")]:n.isActive,[ai(e.exactActiveClass,r.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const s=t.default&&t.default(n);return e.custom?s:oe("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:o.value},s)}}}),Yf=Gf;function Jf(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Xf(e,t){for(const n in t){const r=t[n],o=e[n];if(typeof r=="string"){if(r!==o)return!1}else if(!lt(o)||o.length!==r.length||r.some((s,i)=>s!==o[i]))return!1}return!0}function li(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const ai=(e,t,n)=>e??t??n,Qf=pe({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const r=ze(Ao),o=I(()=>e.route||r.value),s=ze(si,0),i=I(()=>{let c=X(s);const{matched:u}=o.value;let f;for(;(f=u[c])&&!f.components;)c++;return c}),l=I(()=>o.value.matched[i.value]);Mt(si,I(()=>i.value+1)),Mt(Kf,l),Mt(Ao,o);const a=ce();return Fe(()=>[a.value,l.value,e.name],([c,u,f],[d,m,g])=>{u&&(u.instances[f]=c,m&&m!==u&&c&&c===d&&(u.leaveGuards.size||(u.leaveGuards=m.leaveGuards),u.updateGuards.size||(u.updateGuards=m.updateGuards))),c&&u&&(!m||!vn(u,m)||!d)&&(u.enterCallbacks[f]||[]).forEach(b=>b(c))},{flush:"post"}),()=>{const c=o.value,u=e.name,f=l.value,d=f&&f.components[u];if(!d)return ci(n.default,{Component:d,route:c});const m=f.props[u],g=m?m===!0?c.params:typeof m=="function"?m(c):m:null,E=oe(d,me({},g,t,{onVnodeUnmounted:A=>{A.component.isUnmounted&&(f.instances[u]=null)},ref:a}));return ci(n.default,{Component:E,route:c})||E}}});function ci(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Zf=Qf;function ed(e){const t=Df(e.routes,e),n=e.parseQuery||Wf,r=e.stringifyQuery||oi,o=e.history,s=xn(),i=xn(),l=xn(),a=on(_t);let c=_t;cn&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const u=lo.bind(null,x=>""+x),f=lo.bind(null,df),d=lo.bind(null,Un);function m(x,U){let D,q;return Nl(x)?(D=t.getRecordMatcher(x),q=U):q=x,t.addRoute(q,D)}function g(x){const U=t.getRecordMatcher(x);U&&t.removeRoute(U)}function b(){return t.getRoutes().map(x=>x.record)}function E(x){return!!t.getRecordMatcher(x)}function A(x,U){if(U=me({},U||a.value),typeof x=="string"){const p=ao(n,x,U.path),_=t.resolve({path:p.path},U),k=o.createHref(p.fullPath);return me(p,_,{params:d(_.params),hash:Un(p.hash),redirectedFrom:void 0,href:k})}let D;if(x.path!=null)D=me({},x,{path:ao(n,x.path,U.path).path});else{const p=me({},x.params);for(const _ in p)p[_]==null&&delete p[_];D=me({},x,{params:f(p)}),U.params=f(U.params)}const q=t.resolve(D,U),ue=x.hash||"";q.params=u(d(q.params));const ge=mf(r,me({},x,{hash:cf(ue),path:q.path})),h=o.createHref(ge);return me({fullPath:ge,hash:ue,query:r===oi?Vf(x.query):x.query||{}},q,{redirectedFrom:void 0,href:h})}function T(x){return typeof x=="string"?ao(n,x,a.value.path):me({},x)}function v(x,U){if(c!==x)return yn(8,{from:U,to:x})}function C(x){return H(x)}function B(x){return C(me(T(x),{replace:!0}))}function O(x){const U=x.matched[x.matched.length-1];if(U&&U.redirect){const{redirect:D}=U;let q=typeof D=="function"?D(x):D;return typeof q=="string"&&(q=q.includes("?")||q.includes("#")?q=T(q):{path:q},q.params={}),me({query:x.query,hash:x.hash,params:q.path!=null?{}:x.params},q)}}function H(x,U){const D=c=A(x),q=a.value,ue=x.state,ge=x.force,h=x.replace===!0,p=O(D);if(p)return H(me(T(p),{state:typeof p=="object"?me({},ue,p.state):ue,force:ge,replace:h}),U||D);const _=D;_.redirectedFrom=U;let k;return!ge&&gf(r,q,D)&&(k=yn(16,{to:_,from:q}),We(q,q,!0,!1)),(k?Promise.resolve(k):L(_,q)).catch(S=>yt(S)?yt(S,2)?S:He(S):V(S,_,q)).then(S=>{if(S){if(yt(S,2))return H(me({replace:h},T(S.to),{state:typeof S.to=="object"?me({},ue,S.to.state):ue,force:ge}),U||_)}else S=w(_,q,!0,h,ue);return W(_,q,S),S})}function y(x,U){const D=v(x,U);return D?Promise.reject(D):Promise.resolve()}function G(x){const U=St.values().next().value;return U&&typeof U.runWithContext=="function"?U.runWithContext(x):x()}function L(x,U){let D;const[q,ue,ge]=td(x,U);D=co(q.reverse(),"beforeRouteLeave",x,U);for(const p of q)p.leaveGuards.forEach(_=>{D.push(It(_,x,U))});const h=y.bind(null,x,U);return D.push(h),Oe(D).then(()=>{D=[];for(const p of s.list())D.push(It(p,x,U));return D.push(h),Oe(D)}).then(()=>{D=co(ue,"beforeRouteUpdate",x,U);for(const p of ue)p.updateGuards.forEach(_=>{D.push(It(_,x,U))});return D.push(h),Oe(D)}).then(()=>{D=[];for(const p of ge)if(p.beforeEnter)if(lt(p.beforeEnter))for(const _ of p.beforeEnter)D.push(It(_,x,U));else D.push(It(p.beforeEnter,x,U));return D.push(h),Oe(D)}).then(()=>(x.matched.forEach(p=>p.enterCallbacks={}),D=co(ge,"beforeRouteEnter",x,U,G),D.push(h),Oe(D))).then(()=>{D=[];for(const p of i.list())D.push(It(p,x,U));return D.push(h),Oe(D)}).catch(p=>yt(p,8)?p:Promise.reject(p))}function W(x,U,D){l.list().forEach(q=>G(()=>q(x,U,D)))}function w(x,U,D,q,ue){const ge=v(x,U);if(ge)return ge;const h=U===_t,p=cn?history.state:{};D&&(q||h?o.replace(x.fullPath,me({scroll:h&&p&&p.scroll},ue)):o.push(x.fullPath,ue)),a.value=x,We(x,U,D,h),He()}let M;function te(){M||(M=o.listen((x,U,D)=>{if(!ut.listening)return;const q=A(x),ue=O(q);if(ue){H(me(ue,{replace:!0}),q).catch(Nn);return}c=q;const ge=a.value;cn&&Sf(Js(ge.fullPath,D.delta),Vr()),L(q,ge).catch(h=>yt(h,12)?h:yt(h,2)?(H(h.to,q).then(p=>{yt(p,20)&&!D.delta&&D.type===Wn.pop&&o.go(-1,!1)}).catch(Nn),Promise.reject()):(D.delta&&o.go(-D.delta,!1),V(h,q,ge))).then(h=>{h=h||w(q,ge,!1),h&&(D.delta&&!yt(h,8)?o.go(-D.delta,!1):D.type===Wn.pop&&yt(h,20)&&o.go(-1,!1)),W(q,ge,h)}).catch(Nn)}))}let ie=xn(),R=xn(),Y;function V(x,U,D){He(x);const q=R.list();return q.length?q.forEach(ue=>ue(x,U,D)):console.error(x),Promise.reject(x)}function Re(){return Y&&a.value!==_t?Promise.resolve():new Promise((x,U)=>{ie.add([x,U])})}function He(x){return Y||(Y=!x,te(),ie.list().forEach(([U,D])=>x?D(x):U()),ie.reset()),x}function We(x,U,D,q){const{scrollBehavior:ue}=e;if(!cn||!ue)return Promise.resolve();const ge=!D&&xf(Js(x.fullPath,0))||(q||!D)&&history.state&&history.state.scroll||null;return _n().then(()=>ue(x,U,ge)).then(h=>h&&Cf(h)).catch(h=>V(h,x,U))}const Be=x=>o.go(x);let Ct;const St=new Set,ut={currentRoute:a,listening:!0,addRoute:m,removeRoute:g,hasRoute:E,getRoutes:b,resolve:A,options:e,push:C,replace:B,go:Be,back:()=>Be(-1),forward:()=>Be(1),beforeEach:s.add,beforeResolve:i.add,afterEach:l.add,onError:R.add,isReady:Re,install(x){const U=this;x.component("RouterLink",Yf),x.component("RouterView",Zf),x.config.globalProperties.$router=U,Object.defineProperty(x.config.globalProperties,"$route",{enumerable:!0,get:()=>X(a)}),cn&&!Ct&&a.value===_t&&(Ct=!0,C(o.location).catch(ue=>{}));const D={};for(const ue in _t)Object.defineProperty(D,ue,{get:()=>a.value[ue],enumerable:!0});x.provide(Kr,U),x.provide(Jo,Ui(D)),x.provide(Ao,a);const q=x.unmount;St.add(x),x.unmount=function(){St.delete(x),St.size<1&&(c=_t,M&&M(),M=null,a.value=_t,Ct=!1,Y=!1),q()}}};function Oe(x){return x.reduce((U,D)=>U.then(()=>G(D)),Promise.resolve())}return ut}function td(e,t){const n=[],r=[],o=[],s=Math.max(t.matched.length,e.matched.length);for(let i=0;i vn(c,l))?r.push(l):n.push(l));const a=e.matched[i];a&&(t.matched.find(c=>vn(c,a))||o.push(a))}return[n,r,o]}function sn(){return ze(Kr)}function Ut(){return ze(Jo)}var Xo=Symbol(""),vt=()=>{const e=ze(Xo);if(!e)throw new Error("useClientData() is called without provider.");return e},nd=()=>vt().pageComponent,mn=()=>vt().pageData,pt=()=>vt().pageFrontmatter,rd=()=>vt().pageHead,od=()=>vt().pageLang,sd=()=>vt().pageLayout,wn=()=>vt().routeLocale,id=()=>vt().routes,Fl=()=>vt().siteData,Qo=()=>vt().siteLocaleData,ld=Symbol(""),To=on(Ju),Vn=on(Xu),Bl=e=>{const t=qu(e);if(Vn.value[t])return t;const n=encodeURI(t);return Vn.value[n]?n:To.value[t]||To.value[n]||t},Kn=e=>{const t=Bl(e),n=Vn.value[t]??{...Vn.value["/404.html"],notFound:!0};return{path:t,notFound:!1,...n}},Zo=pe({name:"ClientOnly",setup(e,t){const n=ce(!1);return Me(()=>{n.value=!0}),()=>{var r,o;return n.value?(o=(r=t.slots).default)==null?void 0:o.call(r):null}}}),ad=pe({name:"Content",props:{path:{type:String,required:!1,default:""}},setup(e){const t=nd(),n=I(()=>{if(!e.path)return t.value;const r=Kn(e.path);return bc(()=>r.loader().then(({comp:o})=>o))});return()=>oe(n.value)}}),ct=(e={})=>e,rr=e=>nr(e)?e:`/ios-training/${Ll(e)}`,cd=e=>{if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget){const t=e.currentTarget.getAttribute("target");if(t!=null&&t.match(/\b_blank\b/i))return}return e.preventDefault(),!0}},or=({active:e=!1,activeClass:t="route-link-active",to:n,...r},{slots:o})=>{var a;const s=sn(),i=Bl(n),l=i.startsWith("#")||i.startsWith("?")?i:rr(i);return oe("a",{...r,class:["route-link",{[t]:e}],href:l,onClick:(c={})=>{cd(c)?s.push(n).catch():Promise.resolve()}},(a=o.default)==null?void 0:a.call(o))};or.displayName="RouteLink";or.props={active:Boolean,activeClass:String,to:String};var ud="Layout",fd="en-US",qt=Xn({resolveLayouts:e=>e.reduce((t,n)=>({...t,...n.layouts}),{}),resolvePageHead:(e,t,n)=>{const r=it(t.description)?t.description:n.description,o=[...Array.isArray(t.head)?t.head:[],...n.head,["title",{},e],["meta",{name:"description",content:r}]];return Ku(o)},resolvePageHeadTitle:(e,t)=>[e.title,t.title].filter(n=>!!n).join(" | "),resolvePageLang:(e,t)=>e.lang||t.lang||fd,resolvePageLayout:(e,t)=>{const n=it(e.frontmatter.layout)?e.frontmatter.layout:ud;if(!t[n])throw new Error(`[vuepress] Cannot resolve layout: ${n}`);return t[n]},resolveRouteLocale:(e,t)=>kl(e,t),resolveSiteLocaleData:(e,t)=>{var n;return{...e,...e.locales[t],head:[...((n=e.locales[t])==null?void 0:n.head)??[],...e.head??[]]}}});function qr(e){return Ri()?(xa(e),!0):!1}function gt(e){return typeof e=="function"?e():X(e)}const es=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const dd=Object.prototype.toString,hd=e=>dd.call(e)==="[object Object]",Po=()=>{};function Dl(e,t){function n(...r){return new Promise((o,s)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(s)})}return n}const zl=e=>e();function pd(e,t={}){let n,r,o=Po;const s=l=>{clearTimeout(l),o(),o=Po};return l=>{const a=gt(e),c=gt(t.maxWait);return n&&s(n),a<=0||c!==void 0&&c<=0?(r&&(s(r),r=null),Promise.resolve(l())):new Promise((u,f)=>{o=t.rejectOnCancel?f:u,c&&!r&&(r=setTimeout(()=>{n&&s(n),r=null,u(l())},c)),n=setTimeout(()=>{r&&s(r),r=null,u(l())},a)})}}function md(e=zl){const t=ce(!0);function n(){t.value=!1}function r(){t.value=!0}const o=(...s)=>{t.value&&e(...s)};return{isActive:Mr(t),pause:n,resume:r,eventFilter:o}}function gd(e){let t;function n(){return t||(t=e()),t}return n.reset=async()=>{const r=t;t=void 0,r&&await r},n}function vd(e){return e||Ur()}function yd(e,t=200,n={}){return Dl(pd(t,n),e)}function _d(e,t,n={}){const{eventFilter:r=zl,...o}=n;return Fe(e,Dl(r,t),o)}function bd(e,t,n={}){const{eventFilter:r,...o}=n,{eventFilter:s,pause:i,resume:l,isActive:a}=md(r);return{stop:_d(e,t,{...o,eventFilter:s}),pause:i,resume:l,isActive:a}}function ts(e,t=!0,n){vd()?Me(e,n):t?e():_n(e)}function wd(e,t,n={}){const{immediate:r=!0}=n,o=ce(!1);let s=null;function i(){s&&(clearTimeout(s),s=null)}function l(){o.value=!1,i()}function a(...c){i(),o.value=!0,s=setTimeout(()=>{o.value=!1,s=null,e(...c)},gt(t))}return r&&(o.value=!0,es&&a()),qr(l),{isPending:Mr(o),start:a,stop:l}}function Ed(e=!1,t={}){const{truthyValue:n=!0,falsyValue:r=!1}=t,o=je(e),s=ce(e);function i(l){if(arguments.length)return s.value=l,s.value;{const a=gt(n);return s.value=s.value===a?gt(r):a,s.value}}return o?i:[s,i]}function Qt(e){var t;const n=gt(e);return(t=n==null?void 0:n.$el)!=null?t:n}const Dt=es?window:void 0,jl=es?window.navigator:void 0;function at(...e){let t,n,r,o;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,o]=e,t=Dt):[t,n,r,o]=e,!t)return Po;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const s=[],i=()=>{s.forEach(u=>u()),s.length=0},l=(u,f,d,m)=>(u.addEventListener(f,d,m),()=>u.removeEventListener(f,d,m)),a=Fe(()=>[Qt(t),gt(o)],([u,f])=>{if(i(),!u)return;const d=hd(f)?{...f}:f;s.push(...n.flatMap(m=>r.map(g=>l(u,m,g,d))))},{immediate:!0,flush:"post"}),c=()=>{a(),i()};return qr(c),c}function Cd(){const e=ce(!1),t=Ur();return t&&Me(()=>{e.value=!0},t),e}function Gr(e){const t=Cd();return I(()=>(t.value,!!e()))}function Ul(e,t={}){const{window:n=Dt}=t,r=Gr(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const s=ce(!1),i=c=>{s.value=c.matches},l=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",i):o.removeListener(i))},a=mc(()=>{r.value&&(l(),o=n.matchMedia(gt(e)),"addEventListener"in o?o.addEventListener("change",i):o.addListener(i),s.value=o.matches)});return qr(()=>{a(),l(),o=void 0}),s}function ui(e,t={}){const{controls:n=!1,navigator:r=jl}=t,o=Gr(()=>r&&"permissions"in r);let s;const i=typeof e=="string"?{name:e}:e,l=ce(),a=()=>{s&&(l.value=s.state)},c=gd(async()=>{if(o.value){if(!s)try{s=await r.permissions.query(i),at(s,"change",a),a()}catch{l.value="prompt"}return s}});return c(),n?{state:l,isSupported:o,query:c}:l}function Sd(e={}){const{navigator:t=jl,read:n=!1,source:r,copiedDuring:o=1500,legacy:s=!1}=e,i=Gr(()=>t&&"clipboard"in t),l=ui("clipboard-read"),a=ui("clipboard-write"),c=I(()=>i.value||s),u=ce(""),f=ce(!1),d=wd(()=>f.value=!1,o);function m(){i.value&&A(l.value)?t.clipboard.readText().then(T=>{u.value=T}):u.value=E()}c.value&&n&&at(["copy","cut"],m);async function g(T=gt(r)){c.value&&T!=null&&(i.value&&A(a.value)?await t.clipboard.writeText(T):b(T),u.value=T,f.value=!0,d.start())}function b(T){const v=document.createElement("textarea");v.value=T??"",v.style.position="absolute",v.style.opacity="0",document.body.appendChild(v),v.select(),document.execCommand("copy"),v.remove()}function E(){var T,v,C;return(C=(v=(T=document==null?void 0:document.getSelection)==null?void 0:T.call(document))==null?void 0:v.toString())!=null?C:""}function A(T){return T==="granted"||T==="prompt"}return{isSupported:c,text:u,copied:f,copy:g}}const pr=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},mr="__vueuse_ssr_handlers__",xd=Ld();function Ld(){return mr in pr||(pr[mr]=pr[mr]||{}),pr[mr]}function kd(e,t){return xd[e]||t}function Ad(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const Td={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},fi="vueuse-storage";function Wl(e,t,n,r={}){var o;const{flush:s="pre",deep:i=!0,listenToStorageChanges:l=!0,writeDefaults:a=!0,mergeDefaults:c=!1,shallow:u,window:f=Dt,eventFilter:d,onError:m=L=>{console.error(L)},initOnMounted:g}=r,b=(u?on:ce)(typeof t=="function"?t():t);if(!n)try{n=kd("getDefaultStorage",()=>{var L;return(L=Dt)==null?void 0:L.localStorage})()}catch(L){m(L)}if(!n)return b;const E=gt(t),A=Ad(E),T=(o=r.serializer)!=null?o:Td[A],{pause:v,resume:C}=bd(b,()=>O(b.value),{flush:s,deep:i,eventFilter:d});f&&l&&ts(()=>{at(f,"storage",y),at(f,fi,G),g&&y()}),g||y();function B(L,W){f&&f.dispatchEvent(new CustomEvent(fi,{detail:{key:e,oldValue:L,newValue:W,storageArea:n}}))}function O(L){try{const W=n.getItem(e);if(L==null)B(W,null),n.removeItem(e);else{const w=T.write(L);W!==w&&(n.setItem(e,w),B(W,w))}}catch(W){m(W)}}function H(L){const W=L?L.newValue:n.getItem(e);if(W==null)return a&&E!=null&&n.setItem(e,T.write(E)),E;if(!L&&c){const w=T.read(W);return typeof c=="function"?c(w,E):A==="object"&&!Array.isArray(w)?{...E,...w}:w}else return typeof W!="string"?W:T.read(W)}function y(L){if(!(L&&L.storageArea!==n)){if(L&&L.key==null){b.value=E;return}if(!(L&&L.key!==e)){v();try{(L==null?void 0:L.newValue)!==T.write(b.value)&&(b.value=H(L))}catch(W){m(W)}finally{L?_n(C):C()}}}}function G(L){y(L.detail)}return b}function Pd(e){return Ul("(prefers-color-scheme: dark)",e)}function Rd(e,t,n={}){const{window:r=Dt,...o}=n;let s;const i=Gr(()=>r&&"ResizeObserver"in r),l=()=>{s&&(s.disconnect(),s=void 0)},a=I(()=>Array.isArray(e)?e.map(f=>Qt(f)):[Qt(e)]),c=Fe(a,f=>{if(l(),i.value&&r){s=new ResizeObserver(t);for(const d of f)d&&s.observe(d,o)}},{immediate:!0,flush:"post"}),u=()=>{l(),c()};return qr(u),{isSupported:i,stop:u}}function Od(e,t={width:0,height:0},n={}){const{window:r=Dt,box:o="content-box"}=n,s=I(()=>{var f,d;return(d=(f=Qt(e))==null?void 0:f.namespaceURI)==null?void 0:d.includes("svg")}),i=ce(t.width),l=ce(t.height),{stop:a}=Rd(e,([f])=>{const d=o==="border-box"?f.borderBoxSize:o==="content-box"?f.contentBoxSize:f.devicePixelContentBoxSize;if(r&&s.value){const m=Qt(e);if(m){const g=r.getComputedStyle(m);i.value=Number.parseFloat(g.width),l.value=Number.parseFloat(g.height)}}else if(d){const m=Array.isArray(d)?d:[d];i.value=m.reduce((g,{inlineSize:b})=>g+b,0),l.value=m.reduce((g,{blockSize:b})=>g+b,0)}else i.value=f.contentRect.width,l.value=f.contentRect.height},n);ts(()=>{const f=Qt(e);f&&(i.value="offsetWidth"in f?f.offsetWidth:t.width,l.value="offsetHeight"in f?f.offsetHeight:t.height)});const c=Fe(()=>Qt(e),f=>{i.value=f?t.width:0,l.value=f?t.height:0});function u(){a(),c()}return{width:i,height:l,stop:u}}function Id(e={}){const{window:t=Dt,behavior:n="auto"}=e;if(!t)return{x:ce(0),y:ce(0)};const r=ce(t.scrollX),o=ce(t.scrollY),s=I({get(){return r.value},set(l){scrollTo({left:l,behavior:n})}}),i=I({get(){return o.value},set(l){scrollTo({top:l,behavior:n})}});return at(t,"scroll",()=>{r.value=t.scrollX,o.value=t.scrollY},{capture:!1,passive:!0}),{x:s,y:i}}function $d(e={}){const{window:t=Dt,initialWidth:n=Number.POSITIVE_INFINITY,initialHeight:r=Number.POSITIVE_INFINITY,listenOrientation:o=!0,includeScrollbar:s=!0}=e,i=ce(n),l=ce(r),a=()=>{t&&(s?(i.value=t.innerWidth,l.value=t.innerHeight):(i.value=t.document.documentElement.clientWidth,l.value=t.document.documentElement.clientHeight))};if(a(),ts(a),at("resize",a,{passive:!0}),o){const c=Ul("(orientation: portrait)");Fe(c,()=>a())}return{width:i,height:l}}const di=async(e,t)=>{const{path:n,query:r}=e.currentRoute.value,{scrollBehavior:o}=e.options;e.options.scrollBehavior=void 0,await e.replace({path:n,query:r,hash:t}),e.options.scrollBehavior=o},Nd=({headerLinkSelector:e,headerAnchorSelector:t,delay:n,offset:r=5})=>{const o=sn();at("scroll",yd(()=>{var g,b;const i=Math.max(window.scrollY,document.documentElement.scrollTop,document.body.scrollTop);if(Math.abs(i-0)f.some(A=>A.hash===E.hash));for(let E=0;E =(((g=A.parentElement)==null?void 0:g.offsetTop)??0)-r,C=!T||i<(((b=T.parentElement)==null?void 0:b.offsetTop)??0)-r;if(!(v&&C))continue;const O=decodeURIComponent(o.currentRoute.value.hash),H=decodeURIComponent(A.hash);if(O===H)return;if(u){for(let y=E+1;y {const t=wn();return I(()=>e[t.value]??{})},zd=()=>{const e=id();return I(()=>Object.keys(e.value))},uo=(e,t)=>{var r;const n=(r=(t==null?void 0:t._instance)||Ur())==null?void 0:r.appContext.components;return n?e in n||tt(e)in n||Yn(tt(e))in n:!1},jd=(e,t)=>it(e)&&e.startsWith(t),Vl=e=>jd(e,"/"),Ud="http://.",hi=(e,t)=>{if(Vl(e)||typeof t!="string")return Kn(e);const n=t.slice(0,t.lastIndexOf("/"));return Kn(new URL(`${n}/${encodeURI(e)}`,Ud).pathname)},Kl=e=>new Promise(t=>setTimeout(t,e));var Wd={"/":{backToTop:"Back to top"}};const Vd=pe({name:"BackToTop",setup(){const e=pt(),t=ns(Wd),n=on(),{height:r}=Od(n),{height:o}=$d(),{y:s}=Id(),i=I(()=>e.value.backToTop!==!1&&s.value>100),l=I(()=>s.value/(r.value-o.value)*100);return Me(()=>{n.value=document.body}),()=>oe(bn,{name:"back-to-top"},()=>i.value?oe("button",{type:"button",class:"vp-back-to-top-button","aria-label":t.value.backToTop,onClick:()=>{window.scrollTo({top:0,behavior:"smooth"})}},[oe("span",{class:"vp-scroll-progress",role:"progressbar","aria-labelledby":"loadinglabel","aria-valuenow":l.value},oe("svg",oe("circle",{cx:"26",cy:"26",r:"24",fill:"none",stroke:"currentColor","stroke-width":"4","stroke-dasharray":`${Math.PI*l.value*.48} ${Math.PI*(100-l.value)*.48}`}))),oe("div",{class:"back-to-top-icon"})]):null)}}),Kd=ct({rootComponents:[Vd]}),qd=/\b(?:Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini)/i,Gd=()=>typeof window<"u"&&window.navigator&&"userAgent"in window.navigator&&qd.test(navigator.userAgent),Yd=({delay:e=500,duration:t=2e3,locales:n,selector:r,showInMobile:o})=>{const{copy:s,copied:i}=Sd({legacy:!0,copiedDuring:t}),l=ns(n),a=mn(),c=d=>{if(!d.hasAttribute("copy-code-registered")){const m=document.createElement("button");m.type="button",m.classList.add("vp-copy-code-button"),m.innerHTML='',m.setAttribute("aria-label",l.value.copy),m.setAttribute("data-copied",l.value.copied),d.parentElement&&d.parentElement.insertBefore(m,d),d.setAttribute("copy-code-registered","")}},u=()=>{_n().then(()=>Kl(e)).then(()=>{r.forEach(d=>{document.querySelectorAll(d).forEach(c)})})},f=(d,m,g)=>{let{innerText:b=""}=m;/language-(shellscript|shell|bash|sh|zsh)/.test(d.classList.toString())&&(b=b.replace(/^ *(\$|>) /gm,"")),s(b).then(()=>{g.classList.add("copied"),Fe(i,()=>{g.classList.remove("copied"),g.blur()},{once:!0})})};Me(()=>{const d=!Gd()||o;d&&u(),at("click",m=>{const g=m.target;if(g.matches('div[class*="language-"] > button.copy')){const b=g.parentElement,E=g.nextElementSibling;E&&f(b,E,g)}else if(g.matches('div[class*="language-"] div.vp-copy-icon')){const b=g.parentElement,E=b.parentElement,A=b.nextElementSibling;A&&f(E,A,b)}}),Fe(()=>a.value.path,()=>{d&&u()})})};var Jd={"/":{copy:"Copy code",copied:"Copied"}},Xd=['.theme-default-content div[class*="language-"] pre'];const Qd=500,Zd=2e3,eh=Jd,th=Xd,nh=!1,rh=ct({setup:()=>{Yd({selector:th,locales:eh,duration:Zd,delay:Qd,showInMobile:nh})}}),oh=oe("svg",{class:"external-link-icon",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",x:"0px",y:"0px",viewBox:"0 0 100 100",width:"15",height:"15"},[oe("path",{fill:"currentColor",d:"M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"}),oe("polygon",{fill:"currentColor",points:"45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"})]),sh=pe({name:"ExternalLinkIcon",props:{locales:{type:Object,default:()=>({})}},setup(e){const t=wn(),n=I(()=>e.locales[t.value]??{openInNewWindow:"open in new window"});return()=>oe("span",[oh,oe("span",{class:"external-link-icon-sr-only"},n.value.openInNewWindow)])}});var ih={"/":{openInNewWindow:"open in new window"},"/fr/":{openInNewWindow:"open in new window"}};const lh=ih,ah=ct({enhance({app:e}){e.component("ExternalLinkIcon",oe(sh,{locales:lh}))}});/** + * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT + */const fe={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:''},status:null,set:e=>{const t=fe.isStarted();e=fo(e,fe.settings.minimum,1),fe.status=e===1?null:e;const n=fe.render(!t),r=n.querySelector(fe.settings.barSelector),o=fe.settings.speed,s=fe.settings.easing;return n.offsetWidth,ch(i=>{gr(r,{transform:"translate3d("+pi(e)+"%,0,0)",transition:"all "+o+"ms "+s}),e===1?(gr(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(function(){gr(n,{transition:"all "+o+"ms linear",opacity:"0"}),setTimeout(function(){fe.remove(),i()},o)},o)):setTimeout(()=>i(),o)}),fe},isStarted:()=>typeof fe.status=="number",start:()=>{fe.status||fe.set(0);const e=()=>{setTimeout(()=>{fe.status&&(fe.trickle(),e())},fe.settings.trickleSpeed)};return fe.settings.trickle&&e(),fe},done:e=>!e&&!fe.status?fe:fe.inc(.3+.5*Math.random()).set(1),inc:e=>{let t=fe.status;return t?(typeof e!="number"&&(e=(1-t)*fo(Math.random()*t,.1,.95)),t=fo(t+e,0,.994),fe.set(t)):fe.start()},trickle:()=>fe.inc(Math.random()*fe.settings.trickleRate),render:e=>{if(fe.isRendered())return document.getElementById("nprogress");mi(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=fe.settings.template;const n=t.querySelector(fe.settings.barSelector),r=e?"-100":pi(fe.status||0),o=document.querySelector(fe.settings.parent);return gr(n,{transition:"all 0 linear",transform:"translate3d("+r+"%,0,0)"}),o!==document.body&&mi(o,"nprogress-custom-parent"),o==null||o.appendChild(t),t},remove:()=>{gi(document.documentElement,"nprogress-busy"),gi(document.querySelector(fe.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&uh(e)},isRendered:()=>!!document.getElementById("nprogress")},fo=(e,t,n)=>e n?n:e,pi=e=>(-1+e)*100,ch=function(){const e=[];function t(){const n=e.shift();n&&n(t)}return function(n){e.push(n),e.length===1&&t()}}(),gr=function(){const e=["Webkit","O","Moz","ms"],t={};function n(i){return i.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(l,a){return a.toUpperCase()})}function r(i){const l=document.body.style;if(i in l)return i;let a=e.length;const c=i.charAt(0).toUpperCase()+i.slice(1);let u;for(;a--;)if(u=e[a]+c,u in l)return u;return i}function o(i){return i=n(i),t[i]??(t[i]=r(i))}function s(i,l,a){l=o(l),i.style[l]=a}return function(i,l){for(const a in l){const c=l[a];c!==void 0&&Object.prototype.hasOwnProperty.call(l,a)&&s(i,a,c)}}}(),ql=(e,t)=>(typeof e=="string"?e:rs(e)).indexOf(" "+t+" ")>=0,mi=(e,t)=>{const n=rs(e),r=n+t;ql(n,t)||(e.className=r.substring(1))},gi=(e,t)=>{const n=rs(e);if(!ql(e,t))return;const r=n.replace(" "+t+" "," ");e.className=r.substring(1,r.length-1)},rs=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),uh=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)},fh=()=>{Me(()=>{const e=sn(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||fe.start()}),e.afterEach(n=>{t.add(n.path),fe.done()})})},dh=ct({setup(){fh()}}),hh=JSON.parse(`{"logo":"/logo.png","repo":"worldline/ios-training","locales":{"/":{"selectLanguageName":"English","sidebar":["/","/presentation/","/swift-part1/","/swift-part2/","/ui-development/","/api-communication/","/persist-data/","/mini-project/","/to-go-further/"]},"/fr/":{"selectLanguageName":"Français","sidebar":["/fr/","/fr/presentation/","/fr/swift-part1/","/fr/swift-part2/","/fr/ui-development/","/fr/api-communication/","/fr/persist-data/","/fr/mini-project/","/fr/to-go-further/"]}},"colorMode":"auto","colorModeSwitch":true,"navbar":[],"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","sidebar":"auto","sidebarDepth":2,"editLink":true,"editLinkText":"Edit this page","lastUpdated":true,"lastUpdatedText":"Last Updated","contributors":true,"contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),ph=ce(hh),Gl=()=>ph,Yl=Symbol(""),mh=()=>{const e=ze(Yl);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},gh=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},vh=ct({enhance({app:e}){const t=Gl(),n=e._context.provides[Xo],r=I(()=>gh(t.value,n.routeLocale.value));e.provide(Yl,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}})}}),yh=pe({__name:"Badge",props:{type:{type:String,required:!1,default:"tip"},text:{type:String,required:!1,default:""},vertical:{type:String,required:!1,default:void 0}},setup(e){return(t,n)=>(j(),Z("span",{class:Ke(["badge",e.type]),style:Jn({verticalAlign:e.vertical})},[ye(t.$slots,"default",{},()=>[mt(Ce(e.text),1)])],6))}}),Se=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n},_h=Se(yh,[["__file","Badge.vue"]]),bh=pe({name:"CodeGroup",slots:Object,setup(e,{slots:t}){const n=ce([]),r=ce(-1),o=Wl("vuepress-code-group",{}),s=I(()=>n.value.map(c=>c.innerText).join(","));Me(()=>{Fe(()=>o.value[s.value],(c=-1)=>{r.value!==c&&(r.value=c)},{immediate:!0}),Fe(r,c=>{o.value[s.value]!==c&&(o.value[s.value]=c)})});const i=(c=r.value)=>{c {c>0?r.value=c-1:r.value=n.value.length-1,n.value[r.value].focus()},a=(c,u)=>{c.key===" "||c.key==="Enter"?(c.preventDefault(),r.value=u):c.key==="ArrowRight"?(c.preventDefault(),i(u)):c.key==="ArrowLeft"&&(c.preventDefault(),l(u))};return()=>{var u;const c=(((u=t.default)==null?void 0:u.call(t))||[]).filter(f=>f.type.name==="CodeGroupItem").map(f=>(f.props===null&&(f.props={}),f));return c.length===0?null:(r.value<0||r.value>c.length-1?(r.value=c.findIndex(f=>f.props.active===""||f.props.active===!0),r.value===-1&&(r.value=0)):c.forEach((f,d)=>{f.props.active=d===r.value}),oe("div",{class:"code-group"},[oe("div",{class:"code-group__nav",role:"tablist"},c.map((f,d)=>{const m=d===r.value;return oe("button",{ref:g=>{g&&(n.value[d]=g)},class:{"code-group__nav-tab":!0,"code-group__nav-tab-active":m},role:"tab",ariaSelected:m,onClick:()=>r.value=d,onKeydown:g=>a(g,d)},f.props.title)})),c]))}}}),wh=pe({name:"CodeGroupItem",__name:"CodeGroupItem",props:{title:{type:String,required:!0},active:{type:Boolean,required:!1,default:!1}},setup(e){return(t,n)=>(j(),Z("div",{class:Ke(["code-group-item",{"code-group-item__active":e.active}]),role:"tabpanel"},[ye(t.$slots,"default")],2))}}),Eh=Se(wh,[["__file","CodeGroupItem.vue"]]),Ch=()=>Gl(),Ne=()=>mh(),Jl=Symbol(""),os=()=>{const e=ze(Jl);if(!e)throw new Error("useDarkMode() is called without provider.");return e},Sh=()=>{const e=Ne(),t=Pd(),n=Wl("vuepress-color-scheme",e.value.colorMode),r=I({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});Mt(Jl,r),xh(r)},xh=e=>{const t=(n=e.value)=>{const r=window==null?void 0:window.document.querySelector("html");r==null||r.classList.toggle("dark",n)};Me(()=>{Fe(e,t,{immediate:!0})}),jr(()=>t())},Lh="http://.",kh=()=>{const e=sn(),t=Ut();return n=>{if(n)if(Vl(n))t.path!==n&&e.push(n);else if(Sl(n))window&&window.open(n);else{const r=t.path.slice(0,t.path.lastIndexOf("/"));e.push(new URL(`${r}/${encodeURI(n)}`,Lh).pathname)}}};let ho=null,Ln=null;const Ah={wait:()=>ho,pending:()=>{ho=new Promise(e=>Ln=e)},resolve:()=>{Ln==null||Ln(),ho=null,Ln=null}},Xl=()=>Ah,Ql=e=>{const{notFound:t,meta:n,path:r}=Kn(e);return t?{text:r,link:r}:{text:n.title||r,link:r}},vi=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),Th=(e,t)=>{if(t.hash===e)return!0;const n=vi(t.path),r=vi(e);return n===r},Zl=(e,t)=>e.link&&Th(e.link,t)?!0:e.children?e.children.some(n=>Zl(n,t)):!1,ea=e=>!nr(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,Ph={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},Rh=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=ea(e);return n!==null?Ph[n]:null},Oh=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const s=Rh({docsRepo:e,editLinkPattern:o});return s?s.replace(/:repo/,nr(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,Ll(`${xl(n)}/${r}`)):null},ta=Symbol("sidebarItems"),ss=()=>{const e=ze(ta);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},Ih=()=>{const e=Ne(),t=pt(),n=mn(),r=Ut(),o=I(()=>$h(t.value,e.value,n.value,r.path));Mt(ta,o)},$h=(e,t,n,r)=>{const o=e.sidebar??t.sidebar??"auto",s=e.sidebarDepth??t.sidebarDepth??2;return e.home||o===!1?[]:o==="auto"?na(n,s):Array.isArray(o)?ra(n,r,o,s):Go(o)?Mh(n,r,o,s):[]},Nh=(e,t)=>({text:e.title,link:e.link,children:is(e.children,t)}),is=(e,t)=>t>0?e.map(n=>Nh(n,t-1)):[],na=(e,t)=>[{text:e.title,children:is(e.headers,t)}],ra=(e,t,n,r)=>{const o=s=>{var l;let i;if(it(s)?i=Ql(s):i=s,i.children)return{...i,children:i.children.map(a=>o(a))};if(i.link===t){const a=((l=e.headers[0])==null?void 0:l.level)===1?e.headers[0].children:e.headers;return{...i,children:is(a,r)}}return i};return n.map(s=>o(s))},Mh=(e,t,n,r)=>{const o=kl(n,t),s=n[o]??[];return s==="heading"?na(e,r):ra(e,t,s,r)},Hh="719px",Fh={mobile:Hh};var qn;(function(e){e.MOBILE="mobile"})(qn||(qn={}));var Ei;const Bh={[qn.MOBILE]:Number.parseInt((Ei=Fh.mobile)==null?void 0:Ei.replace("px",""),10)},oa=(e,t)=>{const n=Bh[e];Number.isInteger(n)&&(at("orientationchange",()=>t(n),!1),at("resize",()=>t(n),!1),Me(()=>{t(n)}))},Dh={},zh={class:"theme-default-content"};function jh(e,t){const n=tn("Content");return j(),Z("div",zh,[se(n)])}const Uh=Se(Dh,[["render",jh],["__file","HomeContent.vue"]]),Wh={key:0,class:"features"},Vh=pe({__name:"HomeFeatures",setup(e){const t=pt(),n=I(()=>Array.isArray(t.value.features)?t.value.features:[]);return(r,o)=>n.value.length?(j(),Z("div",Wh,[(j(!0),Z(_e,null,Ft(n.value,s=>(j(),Z("div",{key:s.title,class:"feature"},[ne("h2",null,Ce(s.title),1),ne("p",null,Ce(s.details),1)]))),128))])):ke("",!0)}}),Kh=Se(Vh,[["__file","HomeFeatures.vue"]]),qh=["innerHTML"],Gh=["textContent"],Yh=pe({__name:"HomeFooter",setup(e){const t=pt(),n=I(()=>t.value.footer),r=I(()=>t.value.footerHtml);return(o,s)=>n.value?(j(),Z(_e,{key:0},[r.value?(j(),Z("div",{key:0,class:"footer",innerHTML:n.value},null,8,qh)):(j(),Z("div",{key:1,class:"footer",textContent:Ce(n.value)},null,8,Gh))],64)):ke("",!0)}}),Jh=Se(Yh,[["__file","HomeFooter.vue"]]),Xh=["href","rel","target","aria-label"],Qh=pe({inheritAttrs:!1,__name:"AutoLink",props:{item:{type:Object,required:!0}},setup(e){const t=e,n=Ut(),r=Fl(),{item:o}=Hr(t),s=I(()=>nr(o.value.link)),i=I(()=>!s.value&&Sl(o.value.link)),l=I(()=>{if(!i.value){if(o.value.target)return o.value.target;if(s.value)return"_blank"}}),a=I(()=>l.value==="_blank"),c=I(()=>!s.value&&!i.value&&!a.value),u=I(()=>{if(!i.value){if(o.value.rel)return o.value.rel;if(a.value)return"noopener noreferrer"}}),f=I(()=>o.value.ariaLabel||o.value.text),d=I(()=>{const g=Object.keys(r.value.locales);return g.length?!g.some(b=>b===o.value.link):o.value.link!=="/"}),m=I(()=>c.value?o.value.activeMatch?new RegExp(o.value.activeMatch).test(n.path):d.value?n.path.startsWith(o.value.link):!1:!1);return(g,b)=>{const E=tn("RouteLink"),A=tn("AutoLinkExternalIcon");return c.value?(j(),xe(E,So({key:0,active:m.value,to:X(o).link,"aria-label":f.value},g.$attrs),{default:Le(()=>[ye(g.$slots,"default",{},()=>[ye(g.$slots,"before"),mt(" "+Ce(X(o).text)+" ",1),ye(g.$slots,"after")])]),_:3},16,["active","to","aria-label"])):(j(),Z("a",So({key:1,class:"external-link",href:X(o).link,rel:u.value,target:l.value,"aria-label":f.value},g.$attrs),[ye(g.$slots,"default",{},()=>[ye(g.$slots,"before"),mt(" "+Ce(X(o).text)+" ",1),a.value?(j(),xe(A,{key:0})):ke("",!0),ye(g.$slots,"after")])],16,Xh))}}}),wt=Se(Qh,[["__file","AutoLink.vue"]]),Zh={class:"hero"},ep={key:0,id:"main-title"},tp={key:1,class:"description"},np={key:2,class:"actions"},rp=pe({__name:"HomeHero",setup(e){const t=pt(),n=Qo(),r=os(),o=I(()=>r.value&&t.value.heroImageDark!==void 0?t.value.heroImageDark:t.value.heroImage),s=I(()=>t.value.heroAlt||l.value||"hero"),i=I(()=>t.value.heroHeight||280),l=I(()=>t.value.heroText===null?null:t.value.heroText||n.value.title||"Hello"),a=I(()=>t.value.tagline===null?null:t.value.tagline||n.value.description||"Welcome to your VuePress site"),c=I(()=>Array.isArray(t.value.actions)?t.value.actions.map(({text:f,link:d,type:m="primary"})=>({text:f,link:d,type:m})):[]),u=()=>{if(!o.value)return null;const f=oe("img",{src:rr(o.value),alt:s.value,height:i.value});return t.value.heroImageDark===void 0?f:oe(Zo,()=>f)};return(f,d)=>(j(),Z("header",Zh,[se(u),l.value?(j(),Z("h1",ep,Ce(l.value),1)):ke("",!0),a.value?(j(),Z("p",tp,Ce(a.value),1)):ke("",!0),c.value.length?(j(),Z("p",np,[(j(!0),Z(_e,null,Ft(c.value,m=>(j(),xe(wt,{key:m.text,class:Ke(["action-button",[m.type]]),item:m},null,8,["class","item"]))),128))])):ke("",!0)]))}}),op=Se(rp,[["__file","HomeHero.vue"]]),sp={class:"home"},ip=pe({__name:"Home",setup(e){return(t,n)=>(j(),Z("main",sp,[se(op),se(Kh),se(Uh),se(Jh)]))}}),lp=Se(ip,[["__file","Home.vue"]]),ap=["aria-hidden"],cp=pe({__name:"NavbarBrand",setup(e){const t=wn(),n=Qo(),r=Ne(),o=os(),s=I(()=>r.value.home||t.value),i=I(()=>n.value.title),l=I(()=>o.value&&r.value.logoDark!==void 0?r.value.logoDark:r.value.logo),a=I(()=>r.value.logoAlt??i.value),c=I(()=>i.value.toLocaleUpperCase().trim()===a.value.toLocaleUpperCase().trim()),u=()=>{if(!l.value)return null;const f=oe("img",{class:"logo",src:rr(l.value),alt:a.value});return r.value.logoDark===void 0?f:oe(Zo,()=>f)};return(f,d)=>(j(),xe(X(or),{to:s.value},{default:Le(()=>[se(u),i.value?(j(),Z("span",{key:0,class:Ke(["site-name",{"can-hide":l.value}]),"aria-hidden":c.value},Ce(i.value),11,ap)):ke("",!0)]),_:1},8,["to"]))}}),up=Se(cp,[["__file","NavbarBrand.vue"]]),fp=pe({__name:"DropdownTransition",setup(e){const t=r=>{r.style.height=r.scrollHeight+"px"},n=r=>{r.style.height=""};return(r,o)=>(j(),xe(bn,{name:"dropdown",onEnter:t,onAfterEnter:n,onBeforeLeave:t},{default:Le(()=>[ye(r.$slots,"default")]),_:3}))}}),sa=Se(fp,[["__file","DropdownTransition.vue"]]),dp=["aria-label"],hp={class:"title"},pp=ne("span",{class:"arrow down"},null,-1),mp=["aria-label"],gp={class:"title"},vp={class:"navbar-dropdown"},yp={class:"navbar-dropdown-subtitle"},_p={key:1},bp={class:"navbar-dropdown-subitem-wrapper"},wp=pe({__name:"NavbarDropdown",props:{item:{type:Object,required:!0}},setup(e){const t=e,{item:n}=Hr(t),r=I(()=>n.value.ariaLabel||n.value.text),o=ce(!1),s=Ut();Fe(()=>s.path,()=>{o.value=!1});const i=a=>{a.detail===0?o.value=!o.value:o.value=!1},l=(a,c)=>c[c.length-1]===a;return(a,c)=>(j(),Z("div",{class:Ke(["navbar-dropdown-wrapper",{open:o.value}])},[ne("button",{class:"navbar-dropdown-title",type:"button","aria-label":r.value,onClick:i},[ne("span",hp,Ce(X(n).text),1),pp],8,dp),ne("button",{class:"navbar-dropdown-title-mobile",type:"button","aria-label":r.value,onClick:c[0]||(c[0]=u=>o.value=!o.value)},[ne("span",gp,Ce(X(n).text),1),ne("span",{class:Ke(["arrow",o.value?"down":"right"])},null,2)],8,mp),se(sa,null,{default:Le(()=>[Lr(ne("ul",vp,[(j(!0),Z(_e,null,Ft(X(n).children,u=>(j(),Z("li",{key:u.text,class:"navbar-dropdown-item"},[u.children?(j(),Z(_e,{key:0},[ne("h4",yp,[u.link?(j(),xe(wt,{key:0,item:u,onFocusout:f=>l(u,X(n).children)&&u.children.length===0&&(o.value=!1)},null,8,["item","onFocusout"])):(j(),Z("span",_p,Ce(u.text),1))]),ne("ul",bp,[(j(!0),Z(_e,null,Ft(u.children,f=>(j(),Z("li",{key:f.link,class:"navbar-dropdown-subitem"},[se(wt,{item:f,onFocusout:d=>l(f,u.children)&&l(u,X(n).children)&&(o.value=!1)},null,8,["item","onFocusout"])]))),128))])],64)):(j(),xe(wt,{key:1,item:u,onFocusout:f=>l(u,X(n).children)&&(o.value=!1)},null,8,["item","onFocusout"]))]))),128))],512),[[Or,o.value]])]),_:1})],2))}}),Ep=Se(wp,[["__file","NavbarDropdown.vue"]]),Cp=["aria-label"],Sp=pe({__name:"NavbarItems",setup(e){const t=()=>{const f=Ut(),d=zd(),m=wn(),g=Fl(),b=Qo(),E=Ch(),A=Ne();return I(()=>{const T=Object.keys(g.value.locales);if(T.length<2)return[];const v=f.path,C=f.fullPath;return[{text:`${A.value.selectLanguageText}`,ariaLabel:`${A.value.selectLanguageAriaLabel??A.value.selectLanguageText}`,children:T.map(O=>{var w,M;const H=((w=g.value.locales)==null?void 0:w[O])??{},y=((M=E.value.locales)==null?void 0:M[O])??{},G=`${H.lang}`,L=y.selectLanguageName??G;if(G===b.value.lang)return{text:L,activeMatch:/./,link:f.hash??"#"};const W=v.replace(m.value,O);return{text:L,link:d.value.some(te=>te===W)?C.replace(v,W):y.home??O}})}]})},n=()=>{const f=Ne(),d=I(()=>f.value.repo),m=I(()=>d.value?ea(d.value):null),g=I(()=>d.value&&!nr(d.value)?`https://github.com/${d.value}`:d.value),b=I(()=>g.value?f.value.repoLabel?f.value.repoLabel:m.value===null?"Source":m.value:null);return I(()=>!g.value||!b.value?[]:[{text:b.value,link:g.value}])},r=f=>it(f)?Ql(f):f.children?{...f,children:f.children.map(d=>r(d))}:f,o=()=>{const f=Ne();return I(()=>(f.value.navbar||[]).map(d=>r(d)))},s=ce(!1),i=o(),l=t(),a=n(),c=I(()=>[...i.value,...l.value,...a.value]);oa(qn.MOBILE,f=>{window.innerWidth Ne().value.navbarLabel??"site navigation");return(f,d)=>c.value.length?(j(),Z("nav",{key:0,class:"navbar-items","aria-label":u.value},[(j(!0),Z(_e,null,Ft(c.value,m=>(j(),Z("div",{key:m.text,class:"navbar-item"},["children"in m?(j(),xe(Ep,{key:0,item:m,class:Ke(s.value?"mobile":"")},null,8,["item","class"])):(j(),xe(wt,{key:1,item:m},null,8,["item"]))]))),128))],8,Cp)):ke("",!0)}}),ia=Se(Sp,[["__file","NavbarItems.vue"]]),xp=["title"],Lp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},kp=eu(' ',9),Ap=[kp],Tp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},Pp=ne("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1),Rp=[Pp],Op=pe({__name:"ToggleColorModeButton",setup(e){const t=Ne(),n=os(),r=()=>{n.value=!n.value};return(o,s)=>(j(),Z("button",{class:"toggle-color-mode-button",title:X(t).toggleColorMode,onClick:r},[Lr((j(),Z("svg",Lp,Ap,512)),[[Or,!X(n)]]),Lr((j(),Z("svg",Tp,Rp,512)),[[Or,X(n)]])],8,xp))}}),Ip=Se(Op,[["__file","ToggleColorModeButton.vue"]]),$p=["title"],Np=ne("div",{class:"icon","aria-hidden":"true"},[ne("span"),ne("span"),ne("span")],-1),Mp=[Np],Hp=pe({__name:"ToggleSidebarButton",emits:["toggle"],setup(e){const t=Ne();return(n,r)=>(j(),Z("div",{class:"toggle-sidebar-button",title:X(t).toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:r[0]||(r[0]=o=>n.$emit("toggle"))},Mp,8,$p))}}),Fp=Se(Hp,[["__file","ToggleSidebarButton.vue"]]),Bp=pe({__name:"Navbar",emits:["toggle-sidebar"],setup(e){const t=Ne(),n=ce(null),r=ce(null),o=ce(0),s=I(()=>o.value?{maxWidth:o.value+"px"}:{}),i=(l,a)=>{var f,d,m;const c=(m=(d=(f=l==null?void 0:l.ownerDocument)==null?void 0:f.defaultView)==null?void 0:d.getComputedStyle(l,null))==null?void 0:m[a],u=Number.parseInt(c,10);return Number.isNaN(u)?0:u};return oa(qn.MOBILE,l=>{var c;const a=i(n.value,"paddingLeft")+i(n.value,"paddingRight");window.innerWidth {const c=tn("NavbarSearch");return j(),Z("header",{ref_key:"navbar",ref:n,class:"navbar"},[se(Fp,{onToggle:a[0]||(a[0]=u=>l.$emit("toggle-sidebar"))}),ne("span",{ref_key:"navbarBrand",ref:r},[se(up)],512),ne("div",{class:"navbar-items-wrapper",style:Jn(s.value)},[ye(l.$slots,"before"),se(ia,{class:"can-hide"}),ye(l.$slots,"after"),X(t).colorModeSwitch?(j(),xe(Ip,{key:0})):ke("",!0),se(c)],4)],512)}}}),Dp=Se(Bp,[["__file","Navbar.vue"]]),zp={class:"vp-page-meta"},jp={key:0,class:"vp-meta-item edit-link"},Up=ne("svg",{class:"icon",viewBox:"0 0 1024 1024"},[ne("g",{fill:"currentColor"},[ne("path",{d:"M430.818 653.65a60.46 60.46 0 0 1-50.96-93.281l71.69-114.012 7.773-10.365L816.038 80.138A60.46 60.46 0 0 1 859.225 62a60.46 60.46 0 0 1 43.186 18.138l43.186 43.186a60.46 60.46 0 0 1 0 86.373L588.879 565.55l-8.637 8.637-117.466 68.234a60.46 60.46 0 0 1-31.958 11.229z"}),ne("path",{d:"M728.802 962H252.891A190.883 190.883 0 0 1 62.008 771.98V296.934a190.883 190.883 0 0 1 190.883-192.61h267.754a60.46 60.46 0 0 1 0 120.92H252.891a69.962 69.962 0 0 0-69.098 69.099V771.98a69.962 69.962 0 0 0 69.098 69.098h475.911A69.962 69.962 0 0 0 797.9 771.98V503.363a60.46 60.46 0 1 1 120.922 0V771.98A190.883 190.883 0 0 1 728.802 962z"})])],-1),Wp={class:"vp-meta-item git-info"},Vp={key:0,class:"vp-meta-item last-updated"},Kp={class:"meta-item-label"},qp={class:"meta-item-info"},Gp={key:1,class:"vp-meta-item contributors"},Yp={class:"meta-item-label"},Jp={class:"meta-item-info"},Xp=["title"],Qp=pe({__name:"PageMeta",setup(e){const t=()=>{const a=Ne(),c=mn(),u=pt();return I(()=>{if(!(u.value.editLink??a.value.editLink??!0))return null;const{repo:d,docsRepo:m=d,docsBranch:g="main",docsDir:b="",editLinkText:E}=a.value;if(!m)return null;const A=Oh({docsRepo:m,docsBranch:g,docsDir:b,filePathRelative:c.value.filePathRelative,editLinkPattern:u.value.editLinkPattern??a.value.editLinkPattern});return A?{text:E??"Edit this page",link:A}:null})},n=()=>{const a=Ne(),c=mn(),u=pt();return I(()=>{var m,g;return!(u.value.lastUpdated??a.value.lastUpdated??!0)||!((m=c.value.git)!=null&&m.updatedTime)?null:new Date((g=c.value.git)==null?void 0:g.updatedTime).toLocaleString()})},r=()=>{const a=Ne(),c=mn(),u=pt();return I(()=>{var d;return u.value.contributors??a.value.contributors??!0?((d=c.value.git)==null?void 0:d.contributors)??null:null})},o=Ne(),s=t(),i=n(),l=r();return(a,c)=>{const u=tn("ClientOnly");return j(),Z("footer",zp,[X(s)?(j(),Z("div",jp,[se(wt,{class:"label",item:X(s)},{before:Le(()=>[Up]),_:1},8,["item"])])):ke("",!0),ne("div",Wp,[X(i)?(j(),Z("div",Vp,[ne("span",Kp,Ce(X(o).lastUpdatedText)+": ",1),se(u,null,{default:Le(()=>[ne("span",qp,Ce(X(i)),1)]),_:1})])):ke("",!0),X(l)&&X(l).length?(j(),Z("div",Gp,[ne("span",Yp,Ce(X(o).contributorsText)+": ",1),ne("span",Jp,[(j(!0),Z(_e,null,Ft(X(l),(f,d)=>(j(),Z(_e,{key:d},[ne("span",{class:"contributor",title:`email: ${f.email}`},Ce(f.name),9,Xp),d!==X(l).length-1?(j(),Z(_e,{key:0},[mt(", ")],64)):ke("",!0)],64))),128))])])):ke("",!0)])])}}}),Zp=Se(Qp,[["__file","PageMeta.vue"]]),em=["aria-label"],tm={class:"hint"},nm=ne("span",{class:"arrow left"},null,-1),rm={class:"link"},om={class:"hint"},sm=ne("span",{class:"arrow right"},null,-1),im={class:"link"},lm=pe({__name:"PageNav",setup(e){const t=(f,d)=>{if(f===!1)return null;if(it(f)){const{notFound:m,meta:g,path:b}=hi(f,d);return m?{text:b,link:b}:{text:g.title||b,link:b}}return Go(f)?{...f,link:hi(f.link,d).path}:!1},n=(f,d,m)=>{const g=f.findIndex(b=>b.link===d);if(g!==-1){const b=f[g+m];return b!=null&&b.link?b:null}for(const b of f)if(b.children){const E=n(b.children,d,m);if(E)return E}return null},r=pt(),o=ss(),s=Ne(),i=Ut(),l=kh(),a=I(()=>{const f=t(r.value.prev,i.path);return f!==!1?f:n(o.value,i.path,-1)}),c=I(()=>{const f=t(r.value.next,i.path);return f!==!1?f:n(o.value,i.path,1)}),u=I(()=>Ne().value.pageNavbarLabel??"page navigation");return at("keydown",f=>{f.altKey&&(f.key==="ArrowRight"?c.value&&(l(c.value.link),f.preventDefault()):f.key==="ArrowLeft"&&a.value&&(l(a.value.link),f.preventDefault()))}),(f,d)=>a.value||c.value?(j(),Z("nav",{key:0,class:"vp-page-nav","aria-label":u.value},[a.value?(j(),xe(wt,{key:0,class:"prev",item:a.value},{default:Le(()=>[ne("div",tm,[nm,mt(" "+Ce(X(s).prev??"Prev"),1)]),ne("div",rm,[ne("span",null,Ce(a.value.text),1)])]),_:1},8,["item"])):ke("",!0),c.value?(j(),xe(wt,{key:1,class:"next",item:c.value},{default:Le(()=>[ne("div",om,[mt(Ce(X(s).next??"Next")+" ",1),sm]),ne("div",im,[ne("span",null,Ce(c.value.text),1)])]),_:1},8,["item"])):ke("",!0)],8,em)):ke("",!0)}}),am=Se(lm,[["__file","PageNav.vue"]]),cm={class:"page"},um={class:"theme-default-content"},fm=pe({__name:"Page",setup(e){return(t,n)=>{const r=tn("Content");return j(),Z("main",cm,[ye(t.$slots,"top"),ne("div",um,[ye(t.$slots,"content-top"),se(r),ye(t.$slots,"content-bottom")]),se(Zp),se(am),ye(t.$slots,"bottom")])}}}),dm=Se(fm,[["__file","Page.vue"]]),hm={class:"sidebar-item-children"},pm=pe({__name:"SidebarItem",props:{item:{type:Object,required:!0},depth:{type:Number,required:!1,default:0}},setup(e){const t=e,{item:n,depth:r}=Hr(t),o=Ut(),s=sn(),i=I(()=>Zl(n.value,o)),l=I(()=>({"sidebar-item":!0,"sidebar-heading":r.value===0,active:i.value,collapsible:n.value.collapsible})),a=I(()=>n.value.collapsible?i.value:!0),[c,u]=Ed(a.value),f=m=>{n.value.collapsible&&(m.preventDefault(),u())},d=s.afterEach(m=>{_n(()=>{c.value=a.value})});return zr(()=>{d()}),(m,g)=>{var E;const b=tn("SidebarItem",!0);return j(),Z("li",null,[X(n).link?(j(),xe(wt,{key:0,class:Ke(l.value),item:X(n)},null,8,["class","item"])):(j(),Z("p",{key:1,tabindex:"0",class:Ke(l.value),onClick:f,onKeydown:Hu(f,["enter"])},[mt(Ce(X(n).text)+" ",1),X(n).collapsible?(j(),Z("span",{key:0,class:Ke(["arrow",X(c)?"down":"right"])},null,2)):ke("",!0)],34)),(E=X(n).children)!=null&&E.length?(j(),xe(sa,{key:2},{default:Le(()=>[Lr(ne("ul",hm,[(j(!0),Z(_e,null,Ft(X(n).children,A=>(j(),xe(b,{key:`${X(r)}${A.text}${A.link}`,item:A,depth:X(r)+1},null,8,["item","depth"]))),128))],512),[[Or,X(c)]])]),_:1})):ke("",!0)])}}}),mm=Se(pm,[["__file","SidebarItem.vue"]]),gm={key:0,class:"sidebar-items"},vm=pe({__name:"SidebarItems",setup(e){const t=Ut(),n=ss();return Me(()=>{Fe(()=>t.hash,r=>{const o=document.querySelector(".sidebar");if(!o)return;const s=document.querySelector(`.sidebar a.sidebar-item[href="${t.path}${r}"]`);if(!s)return;const{top:i,height:l}=o.getBoundingClientRect(),{top:a,height:c}=s.getBoundingClientRect();ai+l&&s.scrollIntoView(!1)})}),(r,o)=>X(n).length?(j(),Z("ul",gm,[(j(!0),Z(_e,null,Ft(X(n),s=>(j(),xe(mm,{key:`${s.text}${s.link}`,item:s},null,8,["item"]))),128))])):ke("",!0)}}),ym=Se(vm,[["__file","SidebarItems.vue"]]),_m={class:"sidebar"},bm=pe({__name:"Sidebar",setup(e){return(t,n)=>(j(),Z("aside",_m,[se(ia),ye(t.$slots,"top"),se(ym),ye(t.$slots,"bottom")]))}}),wm=Se(bm,[["__file","Sidebar.vue"]]),Em=pe({__name:"Layout",setup(e){const t=mn(),n=pt(),r=Ne(),o=I(()=>n.value.navbar!==!1&&r.value.navbar!==!1),s=ss(),i=ce(!1),l=E=>{i.value=typeof E=="boolean"?E:!i.value},a={x:0,y:0},c=E=>{a.x=E.changedTouches[0].clientX,a.y=E.changedTouches[0].clientY},u=E=>{const A=E.changedTouches[0].clientX-a.x,T=E.changedTouches[0].clientY-a.y;Math.abs(A)>Math.abs(T)&&Math.abs(A)>40&&(A>0&&a.x<=80?l(!0):l(!1))},f=I(()=>[{"no-navbar":!o.value,"no-sidebar":!s.value.length,"sidebar-open":i.value},n.value.pageClass]);let d;Me(()=>{d=sn().afterEach(()=>{l(!1)})}),jr(()=>{d()});const m=Xl(),g=m.resolve,b=m.pending;return(E,A)=>(j(),Z("div",{class:Ke(["theme-container",f.value]),onTouchstart:c,onTouchend:u},[ye(E.$slots,"navbar",{},()=>[o.value?(j(),xe(Dp,{key:0,onToggleSidebar:l},{before:Le(()=>[ye(E.$slots,"navbar-before")]),after:Le(()=>[ye(E.$slots,"navbar-after")]),_:3})):ke("",!0)]),ne("div",{class:"sidebar-mask",onClick:A[0]||(A[0]=T=>l(!1))}),ye(E.$slots,"sidebar",{},()=>[se(wm,null,{top:Le(()=>[ye(E.$slots,"sidebar-top")]),bottom:Le(()=>[ye(E.$slots,"sidebar-bottom")]),_:3})]),ye(E.$slots,"page",{},()=>[X(n).home?(j(),xe(lp,{key:0})):(j(),xe(bn,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:X(g),onBeforeLeave:X(b)},{default:Le(()=>[(j(),xe(dm,{key:X(t).path},{top:Le(()=>[ye(E.$slots,"page-top")]),"content-top":Le(()=>[ye(E.$slots,"page-content-top")]),"content-bottom":Le(()=>[ye(E.$slots,"page-content-bottom")]),bottom:Le(()=>[ye(E.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34))}}),Cm=Se(Em,[["__file","Layout.vue"]]),Sm={class:"theme-container"},xm={class:"page"},Lm={class:"theme-default-content"},km=ne("h1",null,"404",-1),Am=pe({__name:"NotFound",setup(e){const t=wn(),n=Ne(),r=n.value.notFound??["Not Found"],o=()=>r[Math.floor(Math.random()*r.length)],s=n.value.home??t.value,i=n.value.backToHome??"Back to home";return(l,a)=>(j(),Z("div",Sm,[ne("main",xm,[ne("div",Lm,[km,ne("blockquote",null,Ce(o()),1),se(X(or),{to:X(s)},{default:Le(()=>[mt(Ce(X(i)),1)]),_:1},8,["to"])])])]))}}),Tm=Se(Am,[["__file","NotFound.vue"]]),Pm=ct({enhance({app:e,router:t}){uo("Badge")||e.component("Badge",_h),uo("CodeGroup")||e.component("CodeGroup",bh),uo("CodeGroupItem")||e.component("CodeGroupItem",Eh),e.component("AutoLinkExternalIcon",()=>{const r=e.component("ExternalLinkIcon");return r?oe(r):null}),e.component("NavbarSearch",()=>{const r=e.component("Docsearch")||e.component("SearchBox");return r?oe(r):null});const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Xl().wait(),n(...r))},setup(){Sh(),Ih()},layouts:{Layout:Cm,NotFound:Tm}}),Rm=e=>e instanceof Element?document.activeElement===e&&(["TEXTAREA","SELECT","INPUT"].includes(e.tagName)||e.hasAttribute("contenteditable")):!1,Om=(e,t)=>t.some(n=>{if(it(n))return n===e.key;const{key:r,ctrl:o=!1,shift:s=!1,alt:i=!1}=n;return r===e.key&&o===e.ctrlKey&&s===e.shiftKey&&i===e.altKey}),Im=/[^\x00-\x7F]/,$m=e=>e.split(/\s+/g).map(t=>t.trim()).filter(t=>!!t),yi=e=>e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),_i=(e,t)=>{const n=t.join(" "),r=$m(e);if(Im.test(e))return r.some(i=>n.toLowerCase().indexOf(i)>-1);const o=e.endsWith(" ");return new RegExp(r.map((i,l)=>r.length===l+1&&!o?`(?=.*\\b${yi(i)})`:`(?=.*\\b${yi(i)}\\b)`).join("")+".+","gi").test(n)},Nm=({input:e,hotKeys:t})=>{if(t.value.length===0)return;const n=r=>{e.value&&Om(r,t.value)&&!Rm(r.target)&&(r.preventDefault(),e.value.focus())};Me(()=>{document.addEventListener("keydown",n)}),zr(()=>{document.removeEventListener("keydown",n)})},Mm=[{title:"Welcome",headers:[{level:2,title:"Prerequisites",slug:"prerequisites",link:"#prerequisites",children:[]},{level:2,title:"Useful links",slug:"useful-links",link:"#useful-links",children:[]}],path:"/",pathLocale:"/",extraFields:[]},{title:"Communicate with a REST API",headers:[{level:2,title:"Some useful concepts",slug:"some-useful-concepts",link:"#some-useful-concepts",children:[]},{level:2,title:"PW: call a REST API",slug:"pw-call-a-rest-api",link:"#pw-call-a-rest-api",children:[]}],path:"/api-communication/",pathLocale:"/",extraFields:[]},{title:"Mini project",headers:[{level:2,title:"Requirements",slug:"requirements",link:"#requirements",children:[]},{level:2,title:"Hints",slug:"hints",link:"#hints",children:[]}],path:"/mini-project/",pathLocale:"/",extraFields:[]},{title:"Locally persisting data",headers:[{level:2,title:"UserDefaults",slug:"userdefaults",link:"#userdefaults",children:[]},{level:2,title:"Codable saved in a file",slug:"codable-saved-in-a-file",link:"#codable-saved-in-a-file",children:[]},{level:2,title:"Sophisticated data persistence libraries",slug:"sophisticated-data-persistence-libraries",link:"#sophisticated-data-persistence-libraries",children:[{level:3,title:"Core Data",slug:"core-data",link:"#core-data",children:[]},{level:3,title:"Realm",slug:"realm",link:"#realm",children:[]},{level:3,title:"Firebase datastore (or any other cloud based storage)",slug:"firebase-datastore-or-any-other-cloud-based-storage",link:"#firebase-datastore-or-any-other-cloud-based-storage",children:[]}]},{level:2,title:"PW: complete the official iOS persisting data tutorial",slug:"pw-complete-the-official-ios-persisting-data-tutorial",link:"#pw-complete-the-official-ios-persisting-data-tutorial",children:[]}],path:"/persist-data/",pathLocale:"/",extraFields:[]},{title:"Presentation",headers:[{level:2,title:"Welcompe the world of iOS development",slug:"welcompe-the-world-of-ios-development",link:"#welcompe-the-world-of-ios-development",children:[]},{level:2,title:"History",slug:"history",link:"#history",children:[]},{level:2,title:"Getting started",slug:"getting-started",link:"#getting-started",children:[{level:3,title:"Create a CLI app with swift CLI",slug:"create-a-cli-app-with-swift-cli",link:"#create-a-cli-app-with-swift-cli",children:[]},{level:3,title:"Create an App using Xcode or Swift playgrounds",slug:"create-an-app-using-xcode-or-swift-playgrounds",link:"#create-an-app-using-xcode-or-swift-playgrounds",children:[]}]},{level:2,title:"Swift project managers",slug:"swift-project-managers",link:"#swift-project-managers",children:[]},{level:2,title:"Links and references",slug:"links-and-references",link:"#links-and-references",children:[]}],path:"/presentation/",pathLocale:"/",extraFields:[]},{title:"Swift (part 1)",headers:[{level:2,title:"A quick tour of some features",slug:"a-quick-tour-of-some-features",link:"#a-quick-tour-of-some-features",children:[]},{level:2,title:"Functions",slug:"functions",link:"#functions",children:[]},{level:2,title:"Optionals (aka. Null safety)",slug:"optionals-aka-null-safety",link:"#optionals-aka-null-safety",children:[]},{level:2,title:"Enumerations",slug:"enumerations",link:"#enumerations",children:[]},{level:2,title:"Error management",slug:"error-management",link:"#error-management",children:[]},{level:2,title:"Some features in bulk",slug:"some-features-in-bulk",link:"#some-features-in-bulk",children:[]},{level:2,title:"Exercises",slug:"exercises",link:"#exercises",children:[{level:3,title:"Exercise 1",slug:"exercise-1",link:"#exercise-1",children:[]},{level:3,title:"Exercise 2",slug:"exercise-2",link:"#exercise-2",children:[]},{level:3,title:"Exercise 3",slug:"exercise-3",link:"#exercise-3",children:[]}]},{level:2,title:"Sources",slug:"sources",link:"#sources",children:[]}],path:"/swift-part1/",pathLocale:"/",extraFields:[]},{title:"Swift (part 2)",headers:[{level:2,title:"Object oriented programming features",slug:"object-oriented-programming-features",link:"#object-oriented-programming-features",children:[]},{level:2,title:"Structs",slug:"structs",link:"#structs",children:[]},{level:2,title:"Opaque types",slug:"opaque-types",link:"#opaque-types",children:[]},{level:2,title:"Use structs by default",slug:"use-structs-by-default",link:"#use-structs-by-default",children:[]},{level:2,title:"Functional programming features",slug:"functional-programming-features",link:"#functional-programming-features",children:[]},{level:2,title:"Structured Concurrency",slug:"structured-concurrency",link:"#structured-concurrency",children:[]},{level:2,title:"Generics",slug:"generics",link:"#generics",children:[]},{level:2,title:"Key-paths",slug:"key-paths",link:"#key-paths",children:[]},{level:2,title:"Exercises",slug:"exercises",link:"#exercises",children:[{level:3,title:"Exercise 1",slug:"exercise-1",link:"#exercise-1",children:[]},{level:3,title:"Exercise 2",slug:"exercise-2",link:"#exercise-2",children:[]},{level:3,title:"Exercise 3",slug:"exercise-3",link:"#exercise-3",children:[]}]},{level:2,title:"Sources and more reading",slug:"sources-and-more-reading",link:"#sources-and-more-reading",children:[]}],path:"/swift-part2/",pathLocale:"/",extraFields:[]},{title:"Going further",headers:[{level:2,title:"Server side development",slug:"server-side-development",link:"#server-side-development",children:[]},{level:2,title:"Swift and SwoftUI on the browser",slug:"swift-and-swoftui-on-the-browser",link:"#swift-and-swoftui-on-the-browser",children:[]},{level:2,title:"Advanced Swift",slug:"advanced-swift",link:"#advanced-swift",children:[]},{level:2,title:"Conclusion",slug:"conclusion",link:"#conclusion",children:[]}],path:"/to-go-further/",pathLocale:"/",extraFields:[]},{title:"UI development",headers:[{level:2,title:"SwiftUI",slug:"swiftui",link:"#swiftui",children:[]},{level:2,title:"Prerequisites",slug:"prerequisites",link:"#prerequisites",children:[]},{level:2,title:"Anatomy of a basic view",slug:"anatomy-of-a-basic-view",link:"#anatomy-of-a-basic-view",children:[]},{level:2,title:"A summary of important concepts",slug:"a-summary-of-important-concepts",link:"#a-summary-of-important-concepts",children:[]},{level:2,title:"PW: complete some official SwiftUI tutorials",slug:"pw-complete-some-official-swiftui-tutorials",link:"#pw-complete-some-official-swiftui-tutorials",children:[]}],path:"/ui-development/",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/404.html",pathLocale:"/",extraFields:[]}],Hm=ce(Mm),Fm=()=>Hm,Bm=({searchIndex:e,routeLocale:t,query:n,maxSuggestions:r})=>{const o=I(()=>e.value.filter(s=>s.pathLocale===t.value));return I(()=>{const s=n.value.trim().toLowerCase();if(!s)return[];const i=[],l=(a,c)=>{_i(s,[c.title])&&i.push({link:`${a.path}#${c.slug}`,title:a.title,header:c.title});for(const u of c.children){if(i.length>=r.value)return;l(a,u)}};for(const a of o.value){if(i.length>=r.value)break;if(_i(s,[a.title,...a.extraFields])){i.push({link:a.path,title:a.title});continue}for(const c of a.headers){if(i.length>=r.value)break;l(a,c)}}return i})},Dm=e=>{const t=ce(0);return{focusIndex:t,focusNext:()=>{t.value {t.value>0?t.value-=1:t.value=e.value.length-1}}},zm=pe({name:"SearchBox",props:{locales:{type:Object,default:()=>({})},hotKeys:{type:Array,default:()=>[]},maxSuggestions:{type:Number,default:5}},setup(e){const{locales:t,hotKeys:n,maxSuggestions:r}=Hr(e),o=sn(),s=wn(),i=Fm(),l=ce(null),a=ce(!1),c=ce(""),u=I(()=>t.value[s.value]??{}),f=Bm({searchIndex:i,routeLocale:s,query:c,maxSuggestions:r}),{focusIndex:d,focusNext:m,focusPrev:g}=Dm(f);Nm({input:l,hotKeys:n});const b=I(()=>a.value&&!!f.value.length),E=()=>{b.value&&g()},A=()=>{b.value&&m()},T=v=>{if(!b.value)return;const C=f.value[v];C&&o.push(C.link).then(()=>{c.value="",d.value=0})};return()=>oe("form",{class:"search-box",role:"search"},[oe("input",{ref:l,type:"search",placeholder:u.value.placeholder,autocomplete:"off",spellcheck:!1,value:c.value,onFocus:()=>a.value=!0,onBlur:()=>a.value=!1,onInput:v=>c.value=v.target.value,onKeydown:v=>{switch(v.key){case"ArrowUp":{E();break}case"ArrowDown":{A();break}case"Enter":{v.preventDefault(),T(d.value);break}}}}),b.value&&oe("ul",{class:"suggestions",onMouseleave:()=>d.value=-1},f.value.map(({link:v,title:C,header:B},O)=>oe("li",{class:["suggestion",{focus:d.value===O}],onMouseenter:()=>d.value=O,onMousedown:()=>T(O)},oe("a",{href:v,onClick:H=>H.preventDefault()},[oe("span",{class:"page-title"},C),B&&oe("span",{class:"page-header"},`> ${B}`)]))))])}});var jm=["s","/"],Um={"/":{placeholder:"Search"},"/fr/":{placeholder:"Rechercher"}};const Wm=Um,Vm=jm,Km=5,qm=ct({enhance({app:e}){e.component("SearchBox",t=>oe(zm,{locales:Wm,hotKeys:Vm,maxSuggestions:Km,...t}))}});function Gm(e){return{all:e=e||new Map,on:function(t,n){var r=e.get(t);r?r.push(n):e.set(t,[n])},off:function(t,n){var r=e.get(t);r&&(n?r.splice(r.indexOf(n)>>>0,1):e.set(t,[]))},emit:function(t,n){var r=e.get(t);r&&r.slice().map(function(o){o(n)}),(r=e.get("*"))&&r.slice().map(function(o){o(t,n)})}}}const Ym=()=>{navigator.serviceWorker.getRegistration().then(e=>{e&&e.active&&(e==null||e.addEventListener("updatefound",()=>{window.location.reload(!0)}))})},Jm=async(e,t={},n=!0)=>{const{register:r}=await rt(()=>import("./index-DTEEl-sV.js"),[]);r(e,{ready(o){var s;n&&console.info("[Service Worker]: active"),(s=t.ready)==null||s.call(t,o)},registered(o){var s;n&&console.log("[Service Worker]: registered"),(s=t.registered)==null||s.call(t,o)},cached(o){var s;n&&console.log("[Service Worker]: cached"),(s=t.cached)==null||s.call(t,o)},async updatefound(o){var s;await navigator.serviceWorker.getRegistration()&&(n&&console.log("[Service Worker]: update found"),(s=t.updatefound)==null||s.call(t,o))},updated(o){var s;n&&console.log("[Service Worker]: updated"),(s=t.updated)==null||s.call(t,o)},offline(){var o;n&&console.log("[Service Worker]: offline"),(o=t.offline)==null||o.call(t)},error(o){var s;n&&console.error("[Service Worker]: ",o),(s=t.error)==null||s.call(t,o)}})},Xm=e=>{const t=e.waiting;if(!t)return;const n=new MessageChannel;t.postMessage({type:"SKIP_WAITING"},[n.port2])},la=Symbol(""),Qm=()=>{const e=ze(la);if(!e)throw new Error("usePwaEvent() is called without provider.");return e},Zm=async(e,t)=>Jm(rr(e),{ready(n){t.emit("ready",n)},registered(n){t.emit("registered",n)},cached(n){t.emit("cached",n)},updatefound(n){t.emit("updatefound",n)},updated(n){const r="service-worker-version",o=Number(localStorage.getItem(r)||0);localStorage.setItem(r,(o+1).toString()),localStorage.removeItem("manifest"),t.emit("updated",n)},offline(){t.emit("offline")},error(n){t.emit("error",n)}}),eg=(e,t=!1)=>{const n=Gm();Mt(la,n),Me(async()=>{var o;let r=!1;(o=navigator.serviceWorker)!=null&&o.controller&&navigator.serviceWorker.addEventListener("controllerchange",()=>{r||(r=!0,window.location.reload())}),t&&Ym(),await Zm(e,n)})},tg=()=>{Me(()=>{if(window.matchMedia("(display-mode: standalone)").matches){const t=document.head.querySelector('meta[name="viewport"]');if(t){t.setAttribute("content","width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover");return}const n=document.createElement("meta");n.name="viewport",n.content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover",document.head.appendChild(n)}})},aa=({name:e="",color:t="currentColor"},{slots:n})=>{var r;return oe("svg",{xmlns:"http://www.w3.org/2000/svg",class:["icon",`${e}-icon`],viewBox:"0 0 1024 1024",fill:t,"aria-label":`${e} icon`},(r=n.default)==null?void 0:r.call(n))};aa.displayName="SVGWrapper";const ca=()=>oe(aa,{name:"update"},()=>oe("path",{d:"M949.949 146.25v255.826c0 21.981-13.989 35.97-35.97 35.97H658.154c-13.988 0-25.983-7.992-33.973-21.981-5.997-13.989-4-27.977 7.991-39.97l79.942-77.946c-55.954-51.973-121.918-77.955-199.863-77.955-37.975 0-75.95 8.002-113.924 21.99-37.975 15.985-67.948 37.976-91.934 63.957-25.982 23.987-47.973 53.96-63.957 91.934-29.983 73.955-29.983 153.895 0 227.85 15.984 37.976 37.975 67.947 63.957 91.934 23.986 25.982 53.959 47.973 91.934 63.956 37.974 13.989 75.95 21.991 113.924 21.991 45.967 0 87.942-9.998 127.913-29.982 41.976-17.99 75.951-45.967 101.931-83.943 7.993-4 11.994-5.995 13.989-5.995 5.997 0 9.998 1.994 13.988 5.995l77.958 77.946c3.989 4 5.986 7.993 5.986 11.994 0 1.994-1.996 5.995-3.99 11.994-43.973 51.962-93.941 91.934-151.9 117.914-53.958 25.983-115.92 39.972-185.874 39.972-61.961 0-119.921-11.984-169.89-33.973-57.96-25.985-105.923-57.963-139.896-93.943-35.98-33.972-67.958-81.936-93.94-139.897-45.967-101.93-45.967-237.846 0-339.777 25.982-57.96 57.96-105.923 93.94-139.896 33.973-35.98 81.936-67.958 139.896-93.94 49.968-21.99 107.928-33.974 169.89-33.974 55.963 0 109.923 9.988 161.885 29.973 53.97 21.99 101.933 51.963 139.908 89.938l73.954-73.944c9.987-9.998 23.987-13.988 39.971-8.002 13.988 8.002 21.98 19.995 21.98 33.984z"}));ca.displayName="UpdateIcon";const ng=pe({name:"PwaReadyPopup",props:{locales:{type:Object,required:!0}},slots:Object,setup(e,{slots:t}){const n=ns(e.locales),r=on(),o=I(()=>!!r.value),s=()=>{r.value&&(Xm(r.value),r.value=void 0)};return Me(()=>{Qm().on("updated",l=>{l&&(r.value=l)})}),()=>oe(bn,{name:"popup"},()=>{var i;return((i=t.default)==null?void 0:i.call(t,{isReady:o.value,reload:s}))||(o.value?oe("button",{type:"button",class:"sw-update-popup",tabindex:0,onClick:()=>s()},[n.value.update,oe("span",{class:"icon-wrapper"},oe(ca))]):null)})}});var rg={"/":{install:"Install",iOSInstall:"Tap the share button and then 'Add to Home Screen'",cancel:"Cancel",close:"Close",prevImage:"Previous Image",nextImage:"Next Image",desc:"Description",feature:"Key Features",explain:"This app can be installed on your PC or mobile device. This will allow this web app to look and behave like any other installed app. You will find it in your app lists and be able to pin it to your home screen, start menus or task bars. This installed web app will also be able to safely interact with other apps and your operating system. ",hint:"New content found.",update:"New content is available."}};const og=rg,sg=()=>oe(ng,{locales:og}),ig=ct({setup:()=>{eg("service-worker.js",!1),tg()},rootComponents:[sg]});/*! medium-zoom 1.1.0 | MIT License | https://github.com/francoischalifour/medium-zoom */var Gt=Object.assign||function(e){for(var t=1;t 1&&arguments[1]!==void 0?arguments[1]:{},r=window.Promise||function(w){function M(){}w(M,M)},o=function(w){var M=w.target;if(M===G){g();return}v.indexOf(M)!==-1&&b({target:M})},s=function(){if(!(B||!y.original)){var w=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(O-w)>H.scrollOffset&&setTimeout(g,150)}},i=function(w){var M=w.key||w.keyCode;(M==="Escape"||M==="Esc"||M===27)&&g()},l=function(){var w=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=w;if(w.background&&(G.style.background=w.background),w.container&&w.container instanceof Object&&(M.container=Gt({},H.container,w.container)),w.template){var te=wr(w.template)?w.template:document.querySelector(w.template);M.template=te}return H=Gt({},H,M),v.forEach(function(ie){ie.dispatchEvent(an("medium-zoom:update",{detail:{zoom:L}}))}),L},a=function(){var w=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return e(Gt({},H,w))},c=function(){for(var w=arguments.length,M=Array(w),te=0;te 0?M.reduce(function(R,Y){return[].concat(R,wi(Y))},[]):v;return ie.forEach(function(R){R.classList.remove("medium-zoom-image"),R.dispatchEvent(an("medium-zoom:detach",{detail:{zoom:L}}))}),v=v.filter(function(R){return ie.indexOf(R)===-1}),L},f=function(w,M){var te=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return v.forEach(function(ie){ie.addEventListener("medium-zoom:"+w,M,te)}),C.push({type:"medium-zoom:"+w,listener:M,options:te}),L},d=function(w,M){var te=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return v.forEach(function(ie){ie.removeEventListener("medium-zoom:"+w,M,te)}),C=C.filter(function(ie){return!(ie.type==="medium-zoom:"+w&&ie.listener.toString()===M.toString())}),L},m=function(){var w=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=w.target,te=function(){var R={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},Y=void 0,V=void 0;if(H.container)if(H.container instanceof Object)R=Gt({},R,H.container),Y=R.width-R.left-R.right-H.margin*2,V=R.height-R.top-R.bottom-H.margin*2;else{var Re=wr(H.container)?H.container:document.querySelector(H.container),He=Re.getBoundingClientRect(),We=He.width,Be=He.height,Ct=He.left,St=He.top;R=Gt({},R,{width:We,height:Be,left:Ct,top:St})}Y=Y||R.width-H.margin*2,V=V||R.height-H.margin*2;var ut=y.zoomedHd||y.original,Oe=bi(ut)?Y:ut.naturalWidth||Y,x=bi(ut)?V:ut.naturalHeight||V,U=ut.getBoundingClientRect(),D=U.top,q=U.left,ue=U.width,ge=U.height,h=Math.min(Math.max(ue,Oe),Y)/ue,p=Math.min(Math.max(ge,x),V)/ge,_=Math.min(h,p),k=(-q+(Y-ue)/2+H.margin+R.left)/_,S=(-D+(V-ge)/2+H.margin+R.top)/_,$="scale("+_+") translate3d("+k+"px, "+S+"px, 0)";y.zoomed.style.transform=$,y.zoomedHd&&(y.zoomedHd.style.transform=$)};return new r(function(ie){if(M&&v.indexOf(M)===-1){ie(L);return}var R=function We(){B=!1,y.zoomed.removeEventListener("transitionend",We),y.original.dispatchEvent(an("medium-zoom:opened",{detail:{zoom:L}})),ie(L)};if(y.zoomed){ie(L);return}if(M)y.original=M;else if(v.length>0){var Y=v;y.original=Y[0]}else{ie(L);return}if(y.original.dispatchEvent(an("medium-zoom:open",{detail:{zoom:L}})),O=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,B=!0,y.zoomed=cg(y.original),document.body.appendChild(G),H.template){var V=wr(H.template)?H.template:document.querySelector(H.template);y.template=document.createElement("div"),y.template.appendChild(V.content.cloneNode(!0)),document.body.appendChild(y.template)}if(y.original.parentElement&&y.original.parentElement.tagName==="PICTURE"&&y.original.currentSrc&&(y.zoomed.src=y.original.currentSrc),document.body.appendChild(y.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),y.original.classList.add("medium-zoom-image--hidden"),y.zoomed.classList.add("medium-zoom-image--opened"),y.zoomed.addEventListener("click",g),y.zoomed.addEventListener("transitionend",R),y.original.getAttribute("data-zoom-src")){y.zoomedHd=y.zoomed.cloneNode(),y.zoomedHd.removeAttribute("srcset"),y.zoomedHd.removeAttribute("sizes"),y.zoomedHd.removeAttribute("loading"),y.zoomedHd.src=y.zoomed.getAttribute("data-zoom-src"),y.zoomedHd.onerror=function(){clearInterval(Re),console.warn("Unable to reach the zoom image target "+y.zoomedHd.src),y.zoomedHd=null,te()};var Re=setInterval(function(){y.zoomedHd.complete&&(clearInterval(Re),y.zoomedHd.classList.add("medium-zoom-image--opened"),y.zoomedHd.addEventListener("click",g),document.body.appendChild(y.zoomedHd),te())},10)}else if(y.original.hasAttribute("srcset")){y.zoomedHd=y.zoomed.cloneNode(),y.zoomedHd.removeAttribute("sizes"),y.zoomedHd.removeAttribute("loading");var He=y.zoomedHd.addEventListener("load",function(){y.zoomedHd.removeEventListener("load",He),y.zoomedHd.classList.add("medium-zoom-image--opened"),y.zoomedHd.addEventListener("click",g),document.body.appendChild(y.zoomedHd),te()})}else te()})},g=function(){return new r(function(w){if(B||!y.original){w(L);return}var M=function te(){y.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(y.zoomed),y.zoomedHd&&document.body.removeChild(y.zoomedHd),document.body.removeChild(G),y.zoomed.classList.remove("medium-zoom-image--opened"),y.template&&document.body.removeChild(y.template),B=!1,y.zoomed.removeEventListener("transitionend",te),y.original.dispatchEvent(an("medium-zoom:closed",{detail:{zoom:L}})),y.original=null,y.zoomed=null,y.zoomedHd=null,y.template=null,w(L)};B=!0,document.body.classList.remove("medium-zoom--opened"),y.zoomed.style.transform="",y.zoomedHd&&(y.zoomedHd.style.transform=""),y.template&&(y.template.style.transition="opacity 150ms",y.template.style.opacity=0),y.original.dispatchEvent(an("medium-zoom:close",{detail:{zoom:L}})),y.zoomed.addEventListener("transitionend",M)})},b=function(){var w=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=w.target;return y.original?g():m({target:M})},E=function(){return H},A=function(){return v},T=function(){return y.original},v=[],C=[],B=!1,O=0,H=n,y={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?H=t:(t||typeof t=="string")&&c(t),H=Gt({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},H);var G=ag(H.background);document.addEventListener("click",o),document.addEventListener("keyup",i),document.addEventListener("scroll",s),window.addEventListener("resize",g);var L={open:m,close:g,toggle:b,update:l,clone:a,attach:c,detach:u,on:f,off:d,getOptions:E,getImages:A,getZoomedImage:T};return L};function fg(e,t){t===void 0&&(t={});var n=t.insertAt;if(!(!e||typeof document>"u")){var r=document.head||document.getElementsByTagName("head")[0],o=document.createElement("style");o.type="text/css",n==="top"&&r.firstChild?r.insertBefore(o,r.firstChild):r.appendChild(o),o.styleSheet?o.styleSheet.cssText=e:o.appendChild(document.createTextNode(e))}}var dg=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}";fg(dg);const hg=Symbol("mediumZoom");var pg={};const mg=":not(a) > img",gg=pg,vg=500,yg=ct({enhance({app:e,router:t}){const n=ug(gg);n.refresh=(r=mg)=>{n.detach(),n.attach(r)},e.provide(hg,n),t.afterEach(()=>{Kl(vg).then(()=>n.refresh())})}}),_g={},bg=ct({enhance:({app:e})=>{},setup:()=>{}}),yr=[Dd,Kd,rh,ah,dh,vh,Pm,qm,ig,yg,_g,bg],wg=JSON.parse('{"base":"/ios-training/","lang":"en-US","title":"","description":"","head":[["link",{"rel":"icon","href":"/ios-training/favicon.ico"}],["link",{"rel":"manifest","href":"/ios-training/manifest.webmanifest"}],["meta",{"name":"theme-color","content":"#2176d6"}]],"locales":{"/":{"lang":"en-US","title":"iOS Training","description":"iOS training docs"}}}');var An=on(wg),Eg=Tf,Cg=()=>{const e=ed({history:Eg(xl("/ios-training/")),routes:[{name:"vuepress-route",path:"/:catchAll(.*)",components:{}}],scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{if(t.path!==n.path||n===_t){const r=Kn(t.path);if(r.path!==t.path)return r.path;const o=await r.loader();t.meta={...r.meta,_pageChunk:o}}else t.path===n.path&&(t.meta=n.meta)}),e},Sg=e=>{e.component("ClientOnly",Zo),e.component("Content",ad),e.component("RouteLink",or)},xg=(e,t,n)=>{const r=I(()=>t.currentRoute.value.path),o=Xa((E,A)=>({get(){return E(),t.currentRoute.value.meta._pageChunk},set(T){t.currentRoute.value.meta._pageChunk=T,A()}})),s=I(()=>qt.resolveLayouts(n)),i=I(()=>qt.resolveRouteLocale(An.value.locales,r.value)),l=I(()=>qt.resolveSiteLocaleData(An.value,i.value)),a=I(()=>o.value.comp),c=I(()=>o.value.data),u=I(()=>c.value.frontmatter),f=I(()=>qt.resolvePageHeadTitle(c.value,l.value)),d=I(()=>qt.resolvePageHead(f.value,u.value,l.value)),m=I(()=>qt.resolvePageLang(c.value,l.value)),g=I(()=>qt.resolvePageLayout(c.value,s.value)),b={layouts:s,pageData:c,pageComponent:a,pageFrontmatter:u,pageHead:d,pageHeadTitle:f,pageLang:m,pageLayout:g,redirects:To,routeLocale:i,routePath:r,routes:Vn,siteData:An,siteLocaleData:l};return e.provide(Xo,b),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>u.value},$head:{get:()=>d.value},$headTitle:{get:()=>f.value},$lang:{get:()=>m.value},$page:{get:()=>c.value},$routeLocale:{get:()=>i.value},$site:{get:()=>An.value},$siteLocale:{get:()=>l.value},$withBase:{get:()=>rr}}),b},Lg=()=>{const e=rd(),t=od();let n=[];const r=()=>{e.value.forEach(i=>{const l=kg(i);l&&n.push(l)})},o=()=>{const i=[];return e.value.forEach(l=>{const a=Ag(l);a&&i.push(a)}),i},s=()=>{document.documentElement.lang=t.value;const i=o();n.forEach((l,a)=>{const c=i.findIndex(u=>l.isEqualNode(u));c===-1?(l.remove(),delete n[a]):i.splice(c,1)}),i.forEach(l=>document.head.appendChild(l)),n=[...n.filter(l=>!!l),...i]};Mt(ld,s),Me(()=>{r(),Fe(e,s,{immediate:!1})})},kg=([e,t,n=""])=>{const r=Object.entries(t).map(([l,a])=>it(a)?`[${l}=${JSON.stringify(a)}]`:a===!0?`[${l}]`:"").join(""),o=`head > ${e}${r}`;return Array.from(document.querySelectorAll(o)).find(l=>l.innerText===n)||null},Ag=([e,t,n])=>{if(!it(e))return null;const r=document.createElement(e);return Go(t)&&Object.entries(t).forEach(([o,s])=>{it(s)?r.setAttribute(o,s):s===!0&&r.setAttribute(o,"")}),it(n)&&r.appendChild(document.createTextNode(n)),r},Tg=Du,Pg=async()=>{var n;const e=Tg({name:"Vuepress",setup(){var s;Lg();for(const i of yr)(s=i.setup)==null||s.call(i);const r=yr.flatMap(({rootComponents:i=[]})=>i.map(l=>oe(l))),o=sd();return()=>[oe(o.value),r]}}),t=Cg();Sg(e),xg(e,t,yr);for(const r of yr)await((n=r.enhance)==null?void 0:n.call(r,{app:e,router:t,siteData:An}));return e.use(t),{app:e,router:t}};Pg().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Se as _,ne as a,mt as b,Z as c,Pg as createVueApp,se as d,eu as e,j as o,tn as r}; diff --git a/assets/hello-swiftui-DCdT_J-Q.png b/assets/hello-swiftui-DCdT_J-Q.png new file mode 100644 index 0000000..3857dd6 Binary files /dev/null and b/assets/hello-swiftui-DCdT_J-Q.png differ diff --git a/assets/index-DTEEl-sV.js b/assets/index-DTEEl-sV.js new file mode 100644 index 0000000..6932654 --- /dev/null +++ b/assets/index-DTEEl-sV.js @@ -0,0 +1 @@ +var v=function(){return!!(window.location.hostname==="localhost"||window.location.hostname==="[::1]"||window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/))},c;typeof window<"u"&&(typeof Promise<"u"?c=new Promise(function(t){return window.addEventListener("load",t)}):c={then:function(t){return window.addEventListener("load",t)}});function s(t,n){n===void 0&&(n={});var i=n.registrationOptions;i===void 0&&(i={}),delete n.registrationOptions;var e=function(r){for(var f=[],a=arguments.length-1;a-- >0;)f[a]=arguments[a+1];n&&n[r]&&n[r].apply(n,f)};"serviceWorker"in navigator&&c.then(function(){v()?(l(t,e,i),navigator.serviceWorker.ready.then(function(r){e("ready",r)}).catch(function(r){return o(e,r)})):(u(t,e,i),navigator.serviceWorker.ready.then(function(r){e("ready",r)}).catch(function(r){return o(e,r)}))})}function o(t,n){navigator.onLine||t("offline"),t("error",n)}function u(t,n,i){navigator.serviceWorker.register(t,i).then(function(e){if(n("registered",e),e.waiting){n("updated",e);return}e.onupdatefound=function(){n("updatefound",e);var r=e.installing;r.onstatechange=function(){r.state==="installed"&&(navigator.serviceWorker.controller?n("updated",e):n("cached",e))}}}).catch(function(e){return o(n,e)})}function l(t,n,i){fetch(t).then(function(e){e.status===404?(n("error",new Error("Service worker not found at "+t)),d()):e.headers.get("content-type").indexOf("javascript")===-1?(n("error",new Error("Expected "+t+" to have javascript content-type, but received "+e.headers.get("content-type"))),d()):u(t,n,i)}).catch(function(e){return o(n,e)})}function d(){"serviceWorker"in navigator&&navigator.serviceWorker.ready.then(function(t){t.unregister()}).catch(function(t){return o(emit,t)})}export{s as register,d as unregister}; diff --git a/assets/index.html-5n7MuywO.js b/assets/index.html-5n7MuywO.js new file mode 100644 index 0000000..4faee47 --- /dev/null +++ b/assets/index.html-5n7MuywO.js @@ -0,0 +1,39 @@ +import{_ as o,r as i,o as r,c as l,a as e,b as n,d as t,e as a}from"./app-Bbun9eEO.js";const c={},p=a(' Swift (part 2)
Estimated time
1/2 day
Object oriented programming features
Swift supports most Object Oriented Programming features:
',4),u=e("li",null,[n("Classes that can be instantiated into objects. "),e("ul",null,[e("li",null,"Constructors and destructors are called initializers and deinitializers respectively.")])],-1),d={href:"https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html",target:"_blank",rel:"noopener noreferrer"},h=e("li",null,"Simple inheritance of classes. Multiple inheritance of classes and is not supported.",-1),f=e("li",null,"Inheritance allows one class to use the characteristics of another.",-1),m={href:"https://levelup.gitconnected.com/polymorphism-in-swift",target:"_blank",rel:"noopener noreferrer"},b={href:"https://www.avanderlee.com/swift/composition-inheritance-code-architecture/",target:"_blank",rel:"noopener noreferrer"},k=e("li",null,"Static methods and properties are supported.",-1),g=e("li",null,"Generic types are supported",-1),w=e("em",null,"Protocols",-1),_=e("em",null,"Interfaces",-1),v=e("li",null,"Classes and structs can conform to multiple protocols.",-1),y={href:"https://www.hackingwithswift.com/articles/74/understanding-protocol-associated-types-and-their-constraints",target:"_blank",rel:"noopener noreferrer"},x=e("li",null,[n("They are used a lot by swift developers to the point that there is a programming technique called "),e("strong",null,"Protocol oriented programming"),n(".")],-1),S=e("p",null,"Here are some additional features:",-1),T=e("ul",null,[e("li",null,[n("Extensions allow to add functions and conform to additional protocols outside of the original class, struct or protocol declaration. This has many uses that simplify our code and here are some examples. "),e("ul",null,[e("li",null,"They can add methods to classes from any library that we can use."),e("li",null,"They can define default implementations in protocols.")])]),e("li",null,"Abstract classes are not available")],-1),q={href:"https://swiftfiddle.com/05f4d4d3c8235299a875e08dcb3992f8",target:"_blank",rel:"noopener noreferrer"},I=e("p",null,"In additions to classes, structs in swift are powerful and provide similar features than classes with some exceptions.",-1),E=e("h2",{id:"structs",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#structs"},[e("span",null,"Structs")])],-1),P={href:"https://docs.swift.org/swift-book/LanguageGuide/Subscripts.html",target:"_blank",rel:"noopener noreferrer"},A={href:"https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html",target:"_blank",rel:"noopener noreferrer"},F=e("ul",null,[e("li",null,"Inheritance."),e("li",null,"Type casting (enables you to check and interpret the type of a class instance at runtime)."),e("li",null,"Deinitializers."),e("li",null,"Reference counting allows more than one reference to a class instance (similar to pointers but much less complex to use).")],-1),U={href:"https://swiftfiddle.com/d72ea73dcbae5cc25908c56bdabcf877",target:"_blank",rel:"noopener noreferrer"},C=a(`Opaque types
This feature seems advanced to understand but since it's used a lot in SwiftUI, let's explore a simple explanation and we'll provide some links to study it further.
In a base level, opaque types allow to return Protocols while keeping the concrete type information known by the compiler. It is enabled by prefixing the type with the
some
keyword.Opaque types allow to keep the benefits of abstracting the code on a developer level while maintaining the performance and optimization benefits of concrete typing. In addition to that, they allow the compiler to better handle some cases such as Self or associated type requirements. Please note that explaining all the features that opaque types bring to the code is an advanced topic. For more information and details, please read the articles mentioned in the Sources and more reading section.
For this training, we'll assume that opaque help types the compiler perform better optimizations with protocols, are used in many places in SwiftUI and allow to improve our code in some cases. We'll show below a simple use case where we can define a method that returns an
Equatable
.`,6),j={href:"https://github.com/apple/swift-evolution/blob/main/proposals/0244-opaque-result-types.md",target:"_blank",rel:"noopener noreferrer"},G={href:"https://github.com/apple/swift-evolution/blob/main/proposals/0341-opaque-parameters.md",target:"_blank",rel:"noopener noreferrer"},O=e("h2",{id:"use-structs-by-default",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#use-structs-by-default"},[e("span",null,"Use structs by default")])],-1),W={href:"https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes",target:"_blank",rel:"noopener noreferrer"},z=e("ul",null,[e("li",null,"Use structures by default."),e("li",null,"Use classes when you need Objective-C interoperability."),e("li",null,"Use classes when you need to control the identity of the data you’re modeling."),e("li",null,"Use structures along with protocols to adopt behavior by sharing implementations.")],-1),R=e("p",null,"We note that structures are the default choice mostly because they are value types. This makes the code more predictable because changes cannot come from a parent call. Another advantage of structs is that they are more friendly with functional programming. We'll talk about functional programming in the next section.",-1),L=e("h2",{id:"functional-programming-features",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#functional-programming-features"},[e("span",null,"Functional programming features")])],-1),N={href:"https://flexiple.com/ios/introduction-to-functional-programming-using-swift/",target:"_blank",rel:"noopener noreferrer"},B=e("p",null,"Pure functions are functions that do not have side effects and will thus return always the same output given the same input. Swift allows to create pure functions but does not provide compile time guarantees that a function is pure.",-1),D=e("code",null,"let",-1),K={href:"https://stackoverflow.com/a/24232845",target:"_blank",rel:"noopener noreferrer"},M={href:"https://blog.ndepend.com/declarative-programming-depth/",target:"_blank",rel:"noopener noreferrer"},V={href:"https://swiftfiddle.com/4cebea7bfea3d58600df30f1af325663",target:"_blank",rel:"noopener noreferrer"},H=e("p",null,"Swift has many more features and provides a rich standard library. We'll explore them as needed in the next sections. For now, let's create some UIs in the next chapter.",-1),J=e("h2",{id:"structured-concurrency",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#structured-concurrency"},[e("span",null,"Structured Concurrency")])],-1),Q=e("li",null,"Swift supports writing concurrent code in a structured way.",-1),X=e("li",null,[n("Concurrency means that we execute multiple "),e("strong",null,"tasks"),n(" at the same time. For example, update the UI while the app performs an HTTP request to the server.")],-1),Y=e("li",null,[n("In Swift, we can create concurrent Tasks with the "),e("code",null,"Task"),n(", "),e("code",null,"TaskGroup"),n(" types.")],-1),Z={href:"https://swiftfiddle.com/ce6995e6f1cd9c0fb226df7995b546a5",target:"_blank",rel:"noopener noreferrer"},$={href:"https://oleb.net/2021/structured-concurrency",target:"_blank",rel:"noopener noreferrer"},ee=a("// Source: https://www.educative.io/answers/what-is-opaque-type-in-swift + +// create a function that returns some Equatable +// The compiler fails is the return type is just "Equatable" +func makeInteger() -> some Equatable{ + Int.random(in: 0...10) +} + +let firstInteger = makeInteger() +let secondInteger = makeInteger() + +// this returns "false" because they are of the same concrete type else, Xcode will scream at us. +print(firstInteger == secondInteger) + +func makeString() -> some Equatable{ + "A String" +} +let firstString = makeString() + +// Compiler error because the concrete type is not the same. +print(firstInteger == firstString) +
",1),ne=e("li",null,[n("To summarize "),e("code",null,"async and await"),n(" + "),e("code",null,"Task"),n(" and "),e("code",null,"TaskGroup"),n(" = "),e("strong",null,"Structured Concurrency")],-1),se=e("li",null,"Continuations allow to convert callback code into async/await",-1),te={href:"https://swiftfiddle.com/c34b73f3b260192f63bd8159b9853986",target:"_blank",rel:"noopener noreferrer"},ae={href:"https://swiftfiddle.com/93183f842d0d02756b3e911e9ddc24b8",target:"_blank",rel:"noopener noreferrer"},oe={href:"https://swiftfiddle.com/d5c9039422d60ce14f307623a3d9107e",target:"_blank",rel:"noopener noreferrer"},ie={href:"https://swiftfiddle.com/d62a161afcd623615a27fb09a8b2dc5c",target:"_blank",rel:"noopener noreferrer"},re=a(`
- Structured concurrency means that we write concurrent code using the usual control flow structures (as opposed to callback-based concurrent code)
- In Swift is possible through the
async
andawait
keywords.- When we
await
aTask
, the control flow will continue when it end without blocking theTask
orTaskGroup
on which it is launched.- A function that has uses the
await
keyword must be declared asasync
Structured Concurrency in Playground Book
// Reference: https://stackoverflow.com/a/24066317 +import PlaygroundSupport + +//Playground does not stop at the end of the code +PlaygroundPage.current.needsIndefiniteExecution = true + +func sampleFunc() async { + print("sampleFunc") + try? await Task.sleep(until: .now + .seconds(2)) +} + +Task { + await sampleFunc() + print("done") + // End the playground + PlaygroundPage.current.finishExecution() +} +
Generics
`,3),le={href:"https://swiftfiddle.com/c2619f36b41875606075e1f4baf2b93a",target:"_blank",rel:"noopener noreferrer"},ce=e("h2",{id:"key-paths",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#key-paths"},[e("span",null,"Key-paths")])],-1),pe={href:"https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Key-Path-Expression",target:"_blank",rel:"noopener noreferrer"},ue=e("li",null,[n("They are created with the "),e("code",null,"\\.propertyName"),n(" syntax.")],-1),de=e("li",null,"They are often used to sort, filter, group and map collections and in SwiftUI to bind properties to UI elements.",-1),he={href:"https://swiftfiddle.com/9a220108db372f3c2063d4d7275001a9",target:"_blank",rel:"noopener noreferrer"},fe=e("h2",{id:"exercises",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercises"},[e("span",null,"Exercises")])],-1),me=e("h3",{id:"exercise-1",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercise-1"},[e("span",null,"Exercise 1")])],-1),be={href:"https://swiftfiddle.com/5d65286d3db0ccf08f7ca3bf1cef31fe",target:"_blank",rel:"noopener noreferrer"},ke={class:"custom-container details"},ge=e("summary",null,"Please open to see the solution(s)",-1),we={href:"https://swiftfiddle.com/41469e54bc7c025b003341a0e96f16a3",target:"_blank",rel:"noopener noreferrer"},_e=e("h3",{id:"exercise-2",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercise-2"},[e("span",null,"Exercise 2")])],-1),ve={href:"https://swiftfiddle.com/43fc226645abb5457d26c7176fb6009d",target:"_blank",rel:"noopener noreferrer"},ye={class:"custom-container details"},xe=e("summary",null,"Please open to see the solution(s)",-1),Se={href:"https://swiftfiddle.com/a1227e17989ad80da5137a31aa6dfbeb",target:"_blank",rel:"noopener noreferrer"},Te=e("h3",{id:"exercise-3",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercise-3"},[e("span",null,"Exercise 3")])],-1),qe={href:"https://swiftfiddle.com/d225e7e3b061035b6a1987c3cf2fb3d5",target:"_blank",rel:"noopener noreferrer"},Ie={class:"custom-container details"},Ee=e("summary",null,"Please open to see the solution(s)",-1),Pe={href:"https://swiftfiddle.com/b45e785e8d832058e394f179782b214c",target:"_blank",rel:"noopener noreferrer"},Ae={href:"https://swiftfiddle.com/6aac1fb721c00c565509dded883f7481",target:"_blank",rel:"noopener noreferrer"},Fe=e("h2",{id:"sources-and-more-reading",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#sources-and-more-reading"},[e("span",null,"Sources and more reading")])],-1),Ue={href:"https://docs.swift.org",target:"_blank",rel:"noopener noreferrer"},Ce={href:"https://tanaschita.com/20220206-understanding-opaque-types-in-swift/",target:"_blank",rel:"noopener noreferrer"},je={href:"https://github.com/apple/swift-evolution/blob/main/proposals/0244-opaque-result-types.md",target:"_blank",rel:"noopener noreferrer"},Ge={href:"https://async-await-in-swift.netlify.app/",target:"_blank",rel:"noopener noreferrer"},Oe={href:"https://github.com/SwiftFiddle/swiftfiddle-web/discussions/101",target:"_blank",rel:"noopener noreferrer"};function We(ze,Re){const s=i("ExternalLinkIcon");return r(),l("div",null,[p,e("ul",null,[u,e("li",null,[n("Encapsulation and "),e("a",d,[n("4 access levels"),t(s)]),n(" that range from private to public")]),h,f,e("li",null,[n("Method overriding and "),e("a",m,[n("polymorphism"),t(s)]),n(" and access control.")]),e("li",null,[n("Overloading of operators and functions, "),e("a",b,[n("composition"),t(s)]),n(".")]),k,g,e("li",null,[w,n(" which are the equivalent of "),_,n(". "),e("ul",null,[v,e("li",null,[n("Protocols can have "),e("a",y,[n("associated types"),t(s)]),n(" which is similar to generic types.")]),x])])]),S,T,e("p",null,[e("a",q,[n("this code"),t(s)]),n(" illustrates some of the above features.")]),I,E,e("p",null,[n("In Swift, structs have many similar features with classes. They support properties, methods, "),e("a",P,[n("subscripts"),t(s)]),n(", initializers, extensions and conforming to protocols. The features that are only available in classes "),e("a",A,[n("are as follows"),t(s)]),n(":")]),F,e("p",null,[e("a",U,[n("this code"),t(s)]),n(" sample shows how to use structs with protocols.")]),C,e("p",null,[n("As of Swift 5.1 "),e("a",j,[n("opaque types are only available for return values"),t(s)]),n(". As of Swift 5.7 "),e("a",G,[n("opaque arguments have been implemented"),t(s)])]),O,e("p",null,[n("As surprising as it seems, Apple recommends using "),e("a",W,[n("structs by default instead of classes"),t(s)]),n(". More precisely, when we want to add a new data type, we should not assume that it should be a class by default and check if a structure is more relevant. Apple provides the following recommendations:")]),z,R,L,e("p",null,[n("Functional programming revolves around "),e("a",N,[n("three main concepts"),t(s)]),n(": pure functions, immutable objects and declarative programming.")]),B,e("p",null,[n("Immutable objects can be created using classes or structs with constant properties (declared with "),D,n("). As mentioned above, structs are recommended by default "),e("a",K,[n("and here are other good reasons"),t(s)]),n(". One of the most notable ones is that since structs are passed around by value, thus they help us avoiding side effects.")]),e("p",null,[n("Declarative programming can be easily explained as a way of programming that is centered "),e("a",M,[n("around telling what to do and not how to do it"),t(s)]),n(". This allows to obtain a clearer and more maintainable code than traditional imperative programming. For example, when we want to filter a table, a for loop is not declarative (we say imperative in this case) while the WHERE SQL syntax is considered declarative. Declarative programing is possible in Swift through chaining functions and passing functions as arguments. Indeed, as we have seen earlier, Swift has 1st class support for functions. In addition to that, we can find declarative APIs in the standard Swift library and in Swift UI. The latter will be explored in a different chapter. For now, let's illustrate "),e("a",V,[n("with this code"),t(s)]),n(" how to process a list of strings using only declarative APIs provided by Swift.")]),H,J,e("ul",null,[Q,X,Y,e("li",null,[n("Without Structured concurrency, we would use complex concepts to such as callbacks which make code less readable (have you lived the "),e("a",Z,[n("callback hell"),t(s)]),n(" ?).")]),e("li",null,[n("We say that a code is "),e("a",$,[n("structured"),t(s)]),n(" when we use the well-know control flow structures :if/then/else, loops, functions, lexical scopes for variables. "),ee]),ne,se]),e("p",null,[n("This swift script shows a sample of using "),e("a",te,[n("Task + async/await"),t(s)])]),e("p",null,[n("This swift script shows a sample of using "),e("a",ae,[n("TaskGoup + async/await"),t(s)])]),e("p",null,[n("This swift script shows a sample of "),e("a",oe,[n("TaskGoup cancellation"),t(s)])]),e("p",null,[n("This swift script shows how to "),e("a",ie,[n("convert callbacks into async/await"),t(s)])]),re,e("p",null,[e("a",le,[n("this code"),t(s)]),n(" illustrates some of the above features.")]),ce,e("ul",null,[e("li",null,[e("a",pe,[n("Key-paths"),t(s)]),n(" allow to refer to properties of a type.")]),ue,de]),e("p",null,[e("a",he,[n("this code"),t(s)]),n(" illustrates some of the above features.")]),fe,me,e("p",null,[e("a",be,[n("Please click on this link to view the exercise"),t(s)])]),e("details",ke,[ge,e("p",null,[e("a",we,[n("Solution"),t(s)])])]),_e,e("p",null,[e("a",ve,[n("Please click on this link to view the exercise"),t(s)])]),e("details",ye,[xe,e("p",null,[e("a",Se,[n("Solution"),t(s)])])]),Te,e("p",null,[e("a",qe,[n("Please click on this link to view the exercise"),t(s)])]),e("details",Ie,[Ee,e("p",null,[e("a",Pe,[n("Solution 1"),t(s)])]),e("p",null,[e("a",Ae,[n("Solution 2 with results"),t(s)])])]),Fe,e("ul",null,[e("li",null,[e("a",Ue,[n("Swift official documentation"),t(s)]),n(".")]),e("li",null,[e("a",Ce,[n("Understanding opaque types in Swift"),t(s)])]),e("li",null,[e("a",je,[n("Swift evolution Opaque Result Types"),t(s)])]),e("li",null,[e("a",Ge,[n("Some async examples on SwiftFiddle"),t(s)]),n(" and "),e("a",Oe,[n("how to use Runloop on SwiftFiddle"),t(s)])])])])}const Ne=o(c,[["render",We],["__file","index.html.vue"]]),Be=JSON.parse('{"path":"/swift-part2/","title":"Swift (part 2)","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Object oriented programming features","slug":"object-oriented-programming-features","link":"#object-oriented-programming-features","children":[]},{"level":2,"title":"Structs","slug":"structs","link":"#structs","children":[]},{"level":2,"title":"Opaque types","slug":"opaque-types","link":"#opaque-types","children":[]},{"level":2,"title":"Use structs by default","slug":"use-structs-by-default","link":"#use-structs-by-default","children":[]},{"level":2,"title":"Functional programming features","slug":"functional-programming-features","link":"#functional-programming-features","children":[]},{"level":2,"title":"Structured Concurrency","slug":"structured-concurrency","link":"#structured-concurrency","children":[]},{"level":2,"title":"Generics","slug":"generics","link":"#generics","children":[]},{"level":2,"title":"Key-paths","slug":"key-paths","link":"#key-paths","children":[]},{"level":2,"title":"Exercises","slug":"exercises","link":"#exercises","children":[{"level":3,"title":"Exercise 1","slug":"exercise-1","link":"#exercise-1","children":[]},{"level":3,"title":"Exercise 2","slug":"exercise-2","link":"#exercise-2","children":[]},{"level":3,"title":"Exercise 3","slug":"exercise-3","link":"#exercise-3","children":[]}]},{"level":2,"title":"Sources and more reading","slug":"sources-and-more-reading","link":"#sources-and-more-reading","children":[]}],"git":{"updatedTime":1719480969000,"contributors":[{"name":"yostane","email":"1958676+yostane@users.noreply.github.com","commits":7},{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":6}]},"filePathRelative":"swift-part2/README.md"}');export{Ne as comp,Be as data}; diff --git a/assets/index.html-B8dggsdP.js b/assets/index.html-B8dggsdP.js new file mode 100644 index 0000000..377e5d8 --- /dev/null +++ b/assets/index.html-B8dggsdP.js @@ -0,0 +1 @@ +import{_ as i,r as n,o as l,c as r,a as e,b as t,d as a,e as s}from"./app-Bbun9eEO.js";const c={},d=e("h1",{id:"swift-part-1",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#swift-part-1"},[e("span",null,"Swift (part 1)")])],-1),u=e("div",{class:"custom-container tip"},[e("p",{class:"custom-container-title"},"Estimated time"),e("p",null,"1/2 day")],-1),h=e("p",null,"Swift is the official programming language for developing iOS, iPadOS, macOS, watchOS and AppleTV apps. It can also target other platforms such as Windows, Linux and Android.",-1),f={href:"https://github.com/swiftlang/swift",target:"_blank",rel:"noopener noreferrer"},p=s('
- Generics allow to pass a type as a parameter to a class, struct, enum or function.
- A type parameter can be declares with
<T>
whereT
is the type parameter.- Examples
func printArray<T>(array: [T]) { for item in array { print(item) } }
- Swift can infer the type of the parameter if it is not provided and if it's not ambiguous.
A quick tour of some features
Swift has modern and interesting features. Here are some notable ones:
',3),m={href:"https://swiftfiddle.com/2382a3b3fdc54631140f51bae116dc74",target:"_blank",rel:"noopener noreferrer"},w=e("p",null,"In the following sections, we will delve into more features.",-1),_={class:"custom-container tip"},b=e("p",{class:"custom-container-title"},"++ and -- are removed since swift 3",-1),g={href:"https://github.com/apple/swift-evolution/blob/master/proposals/0004-remove-pre-post-inc-decrement.md",target:"_blank",rel:"noopener noreferrer"},y=s('
- Swift is statically typed and supports implicit typing.
- Static typing: types cannot change on runtime (it is the opposite of dynamic typing).
- Implicit typing: the compiler can infer the type whenever possible.
var
creates mutable variables.let
creates immutable variables or constants.- String interpolation is available with this syntax
\\(expression)
.- Parenthesis are not required in
if
,for
,while
andswitch
statements.if
andswitch
statements are expressions.- for-each is the only type of for loop available.
- Optionals allows to write code free from null pointer errors (also called Null Safety in other languages).
- Functional programming is supported (Higher-order functions and functions as 1st class items, etc.).
- Object oriented programming is supported.
- Interfaces are called protocols and they are used a lot.
- Structures are available and provide a lot of features (More on that later).
Functions
In the this section, the terms argument and parameter are used interchangeably.
The declaration of functions in Swift has the following peculiarities:
',4),v={href:"https://swiftfiddle.com/690a3e3bbe580f524f72358ccdb696da",target:"_blank",rel:"noopener noreferrer"},x=s("
- Parameters are named and ordered. This means that when you call a function, you must specify the name of the arguments in the same order as the declaration.
- A parameter can have different external and internal names by declaring it like this:
externalName internalName: Type
. The external name is also called an argument label.- You can make a parameter anonymous by setting this external name:
_
.- Arguments can have a default value. These are also called optional arguments.
Swift allows to use functions as first class items or citizens. This allows to store function references into variables, pass functions as arguments to other functions and return a function from a function. Here is a brief listing of the these features:
",2),k={href:"https://swiftfiddle.com/5d6b837c869bf23615376bc4cc70bcd1",target:"_blank",rel:"noopener noreferrer"},S=s('
- A function can be assigned to a variable, passed as a function parameter or returned from a function.
- A function type can be expressed as follows:
(typeOfParam1, typeOfParam2, etc) -> returnType
.- The empty return type is
Void
.- We can use
typealias
to shorten writing long types.- Swift supports anonymous functions (also called lambda function) with the following syntax
{ argName1, argName2, etc. in // code }
Let's explore in the next section, one of the most amazing features of Swift which is Optionals.
Optionals (aka. Null safety)
In a nutshell, optionals is a compiler feature that allows you to avoid the infamous Null pointer exception or npe. The Swift compiler provides null safety and reports errors and warnings when we manipulate nullable (also called optional) values. Here is a list of null safety features provided by swift:
The name of null in iOS development
In Swift, the null value is called
nil
- All types are non optional by default. This means that we cannot assign
nil
to a variable or an argument. For example, this code failsvar s: String = nil
.- A type can be made optional by suffixing it with a ?. For example:
var s: String? = nil
.- You cannot call a method or a property of an optional type, unless you do one of those possibilities:
- Use optional chaining with the ? suffix.
- Provide a default value with the ?? operator.
- Unwrap the optional so that it becomes non optional.
- Force unwrap the optional using the ! suffix. This should never be used as it bypasses compiler checks.
',6),T={href:"https://swiftfiddle.com/fa7ad8713475c04666462236db939857",target:"_blank",rel:"noopener noreferrer"},E=s('Never unwrap with !
You must never force unwrap with the !. Use other unwrapping techniques instead. On of the rarest exceptions is with Interface builder's Outlets in UIKit
@IBOutlet var label: UILabel!
. Fortunately, since we are not using UIKit in this training, we will avoid this situation.Enumerations
Enumerations allow to work with a group of values in a type-safe fashion. Swift provides many interesting features to enumerations:
',3),I={href:"https://swiftfiddle.com/d508deb3493e9b572eaf00891c91d8f0",target:"_blank",rel:"noopener noreferrer"},O={href:"https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html",target:"_blank",rel:"noopener noreferrer"},N=e("h2",{id:"error-management",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#error-management"},[e("span",null,"Error management")])],-1),A=e("p",null,[t("Swift provides two ways for error management: "),e("em",null,"Exceptions"),t(" and the "),e("em",null,"Result"),t(" type.")],-1),P=e("code",null,"throw [value]",-1),q=e("code",null,"Error",-1),F={href:"https://www.hackingwithswift.com/example-code/language/how-to-throw-errors-using-strings",target:"_blank",rel:"noopener noreferrer"},W=s("
- When the compiler can infer it, you can omit the name of the enumeration when you use one of its values.
- Switch statements support enumerations.
- You can easily iterate over an enum's values by using
: CaseIterable
.- You can associate values or provide a raw value to enumeration cases. Raw values can be implicitly assigned.
- You can use another enumeration as associated value, this is called recursive enumeration.
We must call throw
when we want to return an error. Throwing in a normal situation is a bad practice.We say that a function throws when it can throw
and exception. It must have thethrows
qualifier.When we call a function that throws, we must precede the call with try
keywordWhen we call a function that throws, we can either propagate its error if it is thrown or handle to stop its propagation. ",4),R=s("The ",1),U={href:"https://swiftfiddle.com/84b40a652f2b31c0b9cd1e0b37b15ca0",target:"_blank",rel:"noopener noreferrer"},Y=e("h2",{id:"some-features-in-bulk",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#some-features-in-bulk"},[e("span",null,"Some features in bulk")])],-1),L={href:"https://swiftfiddle.com/78907b9238bd580e90c9f4b3732e26be",target:"_blank",rel:"noopener noreferrer"},V={href:"https://swiftfiddle.com/507224875d91da9c6257e2e86533b360",target:"_blank",rel:"noopener noreferrer"},B=e("h2",{id:"exercises",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercises"},[e("span",null,"Exercises")])],-1),H=e("h3",{id:"exercise-1",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercise-1"},[e("span",null,"Exercise 1")])],-1),z={href:"https://swiftfiddle.com/6a40668c99d1e2cf079be7525548ca60",target:"_blank",rel:"noopener noreferrer"},C={class:"custom-container details"},D=e("summary",null,"Please open to see the solution(s)",-1),G={href:"https://swiftfiddle.com/4e97fc9476694424b0fbab6dd8118c35",target:"_blank",rel:"noopener noreferrer"},K=e("h3",{id:"exercise-2",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercise-2"},[e("span",null,"Exercise 2")])],-1),M={href:"https://swiftfiddle.com/0e980f44cf6855c63f3a9ce772872dde",target:"_blank",rel:"noopener noreferrer"},j={class:"custom-container details"},J=e("summary",null,"Please open to see the solution(s)",-1),Q={href:"https://swiftfiddle.com/1bb9a747f719e0f35ca470c079a1e453",target:"_blank",rel:"noopener noreferrer"},X=e("h3",{id:"exercise-3",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#exercise-3"},[e("span",null,"Exercise 3")])],-1),Z={href:"https://swiftfiddle.com/2a603ce22c3edc9a2bc0cee8bb65885d",target:"_blank",rel:"noopener noreferrer"},$={class:"custom-container details"},ee=e("summary",null,"Please open to see the solution(s)",-1),te={href:"https://swiftfiddle.com/e3c6f484bffcc5945db5dd43ebc11c84",target:"_blank",rel:"noopener noreferrer"},oe=e("h2",{id:"sources",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#sources"},[e("span",null,"Sources")])],-1),ae={href:"https://docs.swift.org",target:"_blank",rel:"noopener noreferrer"};function se(ie,ne){const o=n("ExternalLinkIcon");return l(),r("div",null,[d,u,h,e("p",null,[t("The source code of the language toolchain is hosted in "),e("a",f,[t("swiftlang/swift"),a(o)]),t(" GitHub repository.")]),p,e("p",null,[e("a",m,[t("this code"),a(o)]),t(" illustrates some of the features listed above.")]),w,e("div",_,[b,e("p",null,[e("a",g,[t("This post"),a(o)]),t(" details all the problems related to using these operators.")])]),y,e("p",null,[e("a",v,[t("This code"),a(o)]),t(" illustrates the above features.")]),x,e("p",null,[e("a",k,[t("This code"),a(o)]),t(" illustrates these features.")]),S,e("p",null,[e("a",T,[t("This code"),a(o)]),t(" illustrates null safety and how to use optional types.")]),E,e("p",null,[e("a",I,[t("This code"),a(o)]),t(" illustrates some enumeration features. For further reading please consult "),e("a",O,[t("the official documentation"),a(o)]),t(".")]),N,A,e("ul",null,[e("li",null,[t("Exceptions provide an alternate return route with the "),P,t(" keyword. "),e("ul",null,[e("li",null,[t("The thrown value must conform the "),q,t(" protocol. We can even throw a "),e("a",F,[t("String"),a(o)]),t(" that way.")]),W])]),R]),e("p",null,[e("a",U,[t("This code"),a(o)]),t(" illustrates error handling features.")]),Y,e("ul",null,[e("li",null,[t("Swift has 3 collection types out of the box: Array, Dictionary and Set. They are illustrated in "),e("a",L,[t("this code"),a(o)])]),e("li",null,[t("We can use tuples in Swift as shown in "),e("a",V,[t("this example"),a(o)])])]),B,H,e("p",null,[e("a",z,[t("Please click on this link to view the exercise"),a(o)])]),e("details",C,[D,e("p",null,[e("a",G,[t("Solution"),a(o)])])]),K,e("p",null,[e("a",M,[t("Please click on this link to view the exercise"),a(o)])]),e("details",j,[J,e("p",null,[e("a",Q,[t("Solution"),a(o)])])]),X,e("p",null,[e("a",Z,[t("Please click on this link to view the exercise"),a(o)])]),e("details",$,[ee,e("p",null,[e("a",te,[t("Solution"),a(o)])])]),oe,e("ul",null,[e("li",null,[e("a",ae,[t("Swift official documentation"),a(o)]),t(".")])])])}const re=i(c,[["render",se],["__file","index.html.vue"]]),ce=JSON.parse('{"path":"/swift-part1/","title":"Swift (part 1)","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"A quick tour of some features","slug":"a-quick-tour-of-some-features","link":"#a-quick-tour-of-some-features","children":[]},{"level":2,"title":"Functions","slug":"functions","link":"#functions","children":[]},{"level":2,"title":"Optionals (aka. Null safety)","slug":"optionals-aka-null-safety","link":"#optionals-aka-null-safety","children":[]},{"level":2,"title":"Enumerations","slug":"enumerations","link":"#enumerations","children":[]},{"level":2,"title":"Error management","slug":"error-management","link":"#error-management","children":[]},{"level":2,"title":"Some features in bulk","slug":"some-features-in-bulk","link":"#some-features-in-bulk","children":[]},{"level":2,"title":"Exercises","slug":"exercises","link":"#exercises","children":[{"level":3,"title":"Exercise 1","slug":"exercise-1","link":"#exercise-1","children":[]},{"level":3,"title":"Exercise 2","slug":"exercise-2","link":"#exercise-2","children":[]},{"level":3,"title":"Exercise 3","slug":"exercise-3","link":"#exercise-3","children":[]}]},{"level":2,"title":"Sources","slug":"sources","link":"#sources","children":[]}],"git":{"updatedTime":1719253649000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":6},{"name":"yostane","email":"1958676+yostane@users.noreply.github.com","commits":5}]},"filePathRelative":"swift-part1/README.md"}');export{re as comp,ce as data}; diff --git a/assets/index.html-C39pB5w2.js b/assets/index.html-C39pB5w2.js new file mode 100644 index 0000000..374e923 --- /dev/null +++ b/assets/index.html-C39pB5w2.js @@ -0,0 +1 @@ +import{_ as a,r as o,o as s,c as r,a as e,b as n,d as i,e as l}from"./app-Bbun9eEO.js";const c={},m=l('Result
type is a an enum that has two possible cases:success(Sucess)
orfailure(Failure)
- The failure value must conform to the
Error
protocol- A
Result
can be handled with usual Swift features for enums:guard
,switch
, etc.- The
Result
type has can be used with the exception style. Itsget()
method returns the success value or throws the error.Welcome
Prerequisites
- Basic knowledge of programming
- Dev environment: macOS and Xcode
Useful links
',4),d={href:"https://developer.apple.com/documentation/",target:"_blank",rel:"noopener noreferrer"},h={href:"https://github.com/worldline/ios-training",target:"_blank",rel:"noopener noreferrer"},p={href:"https://icones8.fr/icon/51974/xcode",target:"_blank",rel:"noopener noreferrer"};function u(f,g){const t=o("ExternalLinkIcon");return s(),r("div",null,[m,e("ul",null,[e("li",null,[e("a",d,[n("Official documentation"),i(t)])]),e("li",null,[e("a",h,[n("GitHub repository for this training"),i(t)])]),e("li",null,[e("a",p,[n("Logo downloaded from icones8"),i(t)])])])])}const b=a(c,[["render",u],["__file","index.html.vue"]]),k=JSON.parse('{"path":"/","title":"Welcome","lang":"en-US","frontmatter":{"home":true,"heroImage":"/logo.png","tagline":"Getting started with iOS development with Swift and SwiftUI","actions":[{"text":"Get started →","link":"/presentation/","type":"primary"}],"features":[{"title":"Swift","details":"Discover the Swift programming language (version 5.10)"},{"title":"SwiftUI","details":"Develop iOS applications"}],"footer":"Worldline, 2021"},"headers":[{"level":2,"title":"Prerequisites","slug":"prerequisites","link":"#prerequisites","children":[]},{"level":2,"title":"Useful links","slug":"useful-links","link":"#useful-links","children":[]}],"git":{"updatedTime":1719246851000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":3},{"name":"yassine benabbas (a527524)","email":"yassine.benabbas@worldline.com","commits":2},{"name":"yostane","email":"1958676+yostane@users.noreply.github.com","commits":2},{"name":"Sowtch","email":"quentin.cptr@gmail.com","commits":1}]},"filePathRelative":"index.md"}');export{b as comp,k as data}; diff --git a/assets/index.html-CfxHtkWF.js b/assets/index.html-CfxHtkWF.js new file mode 100644 index 0000000..25cf745 --- /dev/null +++ b/assets/index.html-CfxHtkWF.js @@ -0,0 +1,8 @@ +import{_ as a,r,o as s,c as i,a as e,b as t,d as o,e as l}from"./app-Bbun9eEO.js";const c={},d=l(`Going further
Server side development
- Vapor is a Swift framework that allows to develop servers
- Install the Vapor cli
brew install vapor
- Create a vapor project
vapor new hello-vapor -n
- Run the server: cd
hello-vapor
andswift run
Building for debugging... +Build complete! (1.25s) +[ NOTICE ] Server starting on http://127.0.0.1:8080 +
- Test the server
➜ curl http://127.0.0.1:8080 +It works! +➜ curl http://127.0.0.1:8080/hello +Hello, world! +
Swift and SwoftUI on the browser
`,7),h={href:"https://swiftwasm.org/",target:"_blank",rel:"noopener noreferrer"},p={href:"https://pad.swiftwasm.org/",target:"_blank",rel:"noopener noreferrer"},u={href:"https://github.com/TokamakUI/Tokamak",target:"_blank",rel:"noopener noreferrer"},f={href:"https://github.com/swiftwebui/SwiftWebUI",target:"_blank",rel:"noopener noreferrer"},w=e("h2",{id:"advanced-swift",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#advanced-swift"},[e("span",null,"Advanced Swift")])],-1),m={href:"https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Actors",target:"_blank",rel:"noopener noreferrer"},b={href:"https://developer.apple.com/documentation/Swift/AdoptingSwift6",target:"_blank",rel:"noopener noreferrer"},g={href:"https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/",target:"_blank",rel:"noopener noreferrer"},v={href:"https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/",target:"_blank",rel:"noopener noreferrer"},_=e("h2",{id:"conclusion",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#conclusion"},[e("span",null,"Conclusion")])],-1),k=e("p",null,"This training was introduction on Swift and SwiftUI. It just scratched the surface of developing for the Apple development. There are many things that we didn't explore such as the accessibility, hardware features such as geolocation and the technical aspects of the developer account (certificates, provisioning profiles, etc.).",-1),S={href:"https://developer.apple.com/videos/",target:"_blank",rel:"noopener noreferrer"},y={href:"https://www.raywenderlich.com/",target:"_blank",rel:"noopener noreferrer"},I={href:"https://www.hackingwithswift.com/",target:"_blank",rel:"noopener noreferrer"},x={href:"https://developer.apple.com/swift-playgrounds/",target:"_blank",rel:"noopener noreferrer"},T={href:"https://developer.apple.com/tutorials/app-dev-training",target:"_blank",rel:"noopener noreferrer"};function U(A,W){const n=r("ExternalLinkIcon");return s(),i("div",null,[d,e("ul",null,[e("li",null,[e("a",h,[t("SwiftWasm"),o(n)]),t(" is a project that allows to run Swift code in the browser using WebAssembly.")]),e("li",null,[e("a",p,[t("SwiftWasm Pad"),o(n)]),t(" is an online editor that allows to write SwiftUI code and run it in the browser. It relies on the "),e("a",u,[t("TokamakUI"),o(n)]),t(" framework which is SwiftUI-compatbile framework(or a re-implementation of SwiftUI) for the web.")]),e("li",null,[e("a",f,[t("SwiftWebUI"),o(n)]),t(" is another project that allows to run a server that renders SwiftUI to the browser. Note that it is considered as a toy project by its creator.")])]),w,e("ul",null,[e("li",null,[e("a",m,[t("Swift actors"),o(n)])]),e("li",null,[e("a",b,[t("Swift 6 strict concurrency"),o(n)])]),e("li",null,[e("a",g,[t("Attributes"),o(n)])]),e("li",null,[e("a",v,[t("Macros allow to run actions on code at compile time"),o(n)])])]),_,k,e("p",null,[t("To go further, it is advised to watch the videos from Apple's "),e("a",S,[t("WWDC"),o(n)]),t(' (WorldWide Developer Conference - pronounced "dubdub dee cee"). There many other resources available online that you should pick and choose depending on the needs. Here are some of them:')]),e("ul",null,[e("li",null,[e("a",y,[t("raywenderlich.com"),o(n)])]),e("li",null,[e("a",I,[t("hackingwithswift.com"),o(n)])]),e("li",null,[e("a",x,[t("Swift playgrounds"),o(n)])]),e("li",null,[e("a",T,[t("Official iOS App Dev Tutorials"),o(n)])])])])}const N=a(c,[["render",U],["__file","index.html.vue"]]),B=JSON.parse('{"path":"/to-go-further/","title":"Going further","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Server side development","slug":"server-side-development","link":"#server-side-development","children":[]},{"level":2,"title":"Swift and SwoftUI on the browser","slug":"swift-and-swoftui-on-the-browser","link":"#swift-and-swoftui-on-the-browser","children":[]},{"level":2,"title":"Advanced Swift","slug":"advanced-swift","link":"#advanced-swift","children":[]},{"level":2,"title":"Conclusion","slug":"conclusion","link":"#conclusion","children":[]}],"git":{"updatedTime":1719433280000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":4},{"name":"yostane","email":"1958676+yostane@users.noreply.github.com","commits":3},{"name":"yassine benabbas (a527524)","email":"yassine.benabbas@worldline.com","commits":1}]},"filePathRelative":"to-go-further/README.md"}');export{N as comp,B as data}; diff --git a/assets/index.html-ChAAQVq_.js b/assets/index.html-ChAAQVq_.js new file mode 100644 index 0000000..590a17e --- /dev/null +++ b/assets/index.html-ChAAQVq_.js @@ -0,0 +1,3 @@ +import{_ as i,r as n,o as r,c as l,a as e,b as a,d as s,e as o}from"./app-Bbun9eEO.js";const d={},c=e("h1",{id:"locally-persisting-data",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#locally-persisting-data"},[e("span",null,"Locally persisting data")])],-1),p=e("div",{class:"custom-container tip"},[e("p",{class:"custom-container-title"},"Estimated time"),e("p",null,"1/4 day")],-1),h=e("p",null,"Persisting data locally consists of keeping app data after the app has been killed and the variables removed from memory. The persisted data offers many advantages. We can use it to show initial data when the app starts and waits for the first batch data to be fetched from the server. It can also be used to allow for offline app usage.",-1),u={class:"custom-container warning"},f=e("p",{class:"custom-container-title"},"iOS isolates app data from other apps",-1),m={href:"https://medium.com/@dinesh.kachhot/different-ways-to-share-data-between-apps-de75a0a46d4a",target:"_blank",rel:"noopener noreferrer"},b=e("p",null,"There are many ways to persist data in SwiftUI that we cover below.",-1),g=e("h2",{id:"userdefaults",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#userdefaults"},[e("span",null,"UserDefaults")])],-1),v={href:"https://www.hackingwithswift.com/books/ios-swiftui/storing-user-settings-with-userdefaults",target:"_blank",rel:"noopener noreferrer"},w=o(`Here is sample code that shows how to persist and load data.
UserDefaults.standard.set(self.tapCount, forKey: "Tap") +@State private var tapCount = UserDefaults.standard.integer(forKey: "Tap") +
Codable
saved in a fileA more advanced and powerful technique is to manually load and persist a
Codable
into a file. This technique is useful if you want to store complex objects (such as the state or model) in a JSON file. There are two steps in this process, the first one consists of decoding / encoding the object from / into JSON usingJSONDecoder().decode
andJSONEncoder().encode
. The second step consists of loading / saving the encoded data and we can think of two ways to achieve this. The first one consists of user defaults'dataForKey:
to load the data andsetObject:ForKey
to persist it. Another one consists of creating and managing a file by the developer using file APIs such asfileHandle.availableData
to load the data from a file anddata.write
to save it.Sophisticated data persistence libraries
For storing data in a database or similar fashion, SQLite is available as a low level library. It is not recommended to use it unless there is a strong performance concern. Instead, it is recommended to use libraries specialized in data persistence. Some can be assimilated to an ORM library (Object Relational Mapper). The remainder of this section describes some of them.
Please be careful about the pricing of cloud storage
Sophisticated databases generally provide cloud storage to provide a complete offer. If you're interested in storing data in the cloud, please take some time to read the pricing page to avoid any bad surprises when your app runs in production.
Core Data
`,8),k={href:"https://developer.apple.com/documentation/coredata",target:"_blank",rel:"noopener noreferrer"},y=e("p",null,"It works similarly as an ORM where classes are mapped into tables. Xcode provides a graphical editor that allows to specify the tables, the relations and generate the necessary code (in Swift or Objective-C).",-1),_=e("figure",null,[e("img",{src:"https://docs-assets.developer.apple.com/published/fbb9767e96/rendered2x-1622022015.png",alt:"Core date editor",tabindex:"0",loading:"lazy"}),e("figcaption",null,"Core date editor")],-1),x={href:"https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui",target:"_blank",rel:"noopener noreferrer"},S=e("h3",{id:"realm",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#realm"},[e("span",null,"Realm")])],-1),T={href:"https://realm.io/",target:"_blank",rel:"noopener noreferrer"},C=o('Firebase datastore (or any other cloud based storage)
As opposed to Realm and Core Data, which are local first databases, Firebase datastore is a cloud first database. This means that Firebase Datastore requires an internet connection to store and load the data. However, the library is simple to use and supports real time updates.
TIP
Firebase datastore is part of a bigger suite of service called Firebase. For example, we can Firebase App Distribution in Firebase, which is a service that allows to deploy and distribute apps without going the burden of using TestFlight.
PW: complete the official iOS persisting data tutorial
This PW shows how to save a
',5),I={href:"https://developer.apple.com/tutorials/app-dev-training/persisting-data",target:"_blank",rel:"noopener noreferrer"};function D(O,F){const t=n("ExternalLinkIcon");return r(),l("div",null,[c,p,h,e("div",u,[f,e("p",null,[a("For security reasons, each app is isolated from the rest of the apps. This is called sandboxing. "),e("a",m,[a("This article"),s(t)]),a(" shows the different ways that allow two or more apps to share their data")])]),b,g,e("p",null,[a("It is a very simple key-value storage that persists data in a file. The API surface is very small and the developer does not need to manage the persisted file. This makes this technique very efficient for simple storage use cases. You can find a short "),e("a",v,[a("guide here"),s(t)]),a(".")]),w,e("p",null,[e("a",k,[a("Core Data"),s(t)]),a(' is the official library to "Persist or cache data on a single device, or sync data to multiple devices with CloudKit". It existed since iOS 3 and Apple continuously updates it to keep it relevant. It also has the reputation of having a steep learning curve, but it remains famous among developers.')]),y,_,e("p",null,[a("Even though Core Date existed before SwiftUI, Apple made sure that both of them can be used together. This article shows "),e("a",x,[a("how to use Core Data in a SwiftUI project"),s(t)]),a(".")]),S,e("p",null,[e("a",T,[a("Realm"),s(t)]),a(" is a high level alternative to SQLite. It can be seen as alternative to Core Data as they seem to provide a similar list of features. Most notably, the possibility to store data locally or in the cloud. The points where Realm wins is that the library seems simpler to learn and to use and that it is also available in Android.")]),C,e("p",null,[e("a",I,[a("https://developer.apple.com/tutorials/app-dev-training/persisting-data"),s(t)])])])}const P=i(d,[["render",D],["__file","index.html.vue"]]),N=JSON.parse('{"path":"/persist-data/","title":"Locally persisting data","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"UserDefaults","slug":"userdefaults","link":"#userdefaults","children":[]},{"level":2,"title":"Codable saved in a file","slug":"codable-saved-in-a-file","link":"#codable-saved-in-a-file","children":[]},{"level":2,"title":"Sophisticated data persistence libraries","slug":"sophisticated-data-persistence-libraries","link":"#sophisticated-data-persistence-libraries","children":[{"level":3,"title":"Core Data","slug":"core-data","link":"#core-data","children":[]},{"level":3,"title":"Realm","slug":"realm","link":"#realm","children":[]},{"level":3,"title":"Firebase datastore (or any other cloud based storage)","slug":"firebase-datastore-or-any-other-cloud-based-storage","link":"#firebase-datastore-or-any-other-cloud-based-storage","children":[]}]},{"level":2,"title":"PW: complete the official iOS persisting data tutorial","slug":"pw-complete-the-official-ios-persisting-data-tutorial","link":"#pw-complete-the-official-ios-persisting-data-tutorial","children":[]}],"git":{"updatedTime":1701178983000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":3}]},"filePathRelative":"persist-data/README.md"}');export{P as comp,N as data}; diff --git a/assets/index.html-Cv2fRjYf.js b/assets/index.html-Cv2fRjYf.js new file mode 100644 index 0000000..48ee3ee --- /dev/null +++ b/assets/index.html-Cv2fRjYf.js @@ -0,0 +1,17 @@ +import{_ as o,r,o as i,c as l,a as e,b as n,d as a,e as t}from"./app-Bbun9eEO.js";const p={},c=t('Codable
in a manually managed file using JSON encoder and filesystem APIs.Communicate with a REST API
Estimated time
1/4 day
Some useful concepts
Communicating with a REST API relies on multiple concepts that we'll cover briefly below:
',4),u={href:"https://www.json.org/json-en.html",target:"_blank",rel:"noopener noreferrer"},d={href:"https://www.redhat.com/en/topics/api/what-is-a-rest-api",target:"_blank",rel:"noopener noreferrer"},h={href:"https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages",target:"_blank",rel:"noopener noreferrer"},m=e("em",null,"body",-1),f={href:"https://developer.apple.com/documentation/swift/codable",target:"_blank",rel:"noopener noreferrer"},k=e("code",null,"Serializable",-1),b=e("li",null,[e("code",null,"async"),n(" and "),e("code",null,"await"),n(": these keywords are used to call an asynchronous function using a synchronous coding fashion. This means that callbacks are needed no more!")],-1),v={href:"https://developer.apple.com/documentation/foundation/urlsession",target:"_blank",rel:"noopener noreferrer"},_={href:"https://github.com/apple/swift-corelibs-foundation",target:"_blank",rel:"noopener noreferrer"},g=e("h2",{id:"pw-call-a-rest-api",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#pw-call-a-rest-api"},[e("span",null,"PW: call a REST API")])],-1),w={href:"https://www.hackingwithswift.com/books/ios-swiftui/sending-and-receiving-codable-data-with-urlsession-and-swiftui",target:"_blank",rel:"noopener noreferrer"},q={href:"https://itunes.apple.com/search?term=taylor+swift&entity=song",target:"_blank",rel:"noopener noreferrer"},T=t(``,1);function y(S,P){const s=r("ExternalLinkIcon");return i(),l("div",null,[c,e("ul",null,[e("li",null,[e("a",u,[n("JSON"),a(s)]),n(": a standard data interchange format used a lot in the HTTP messages of REST APIs.")]),e("li",null,[e("a",d,[n("REST API"),a(s)]),n(": it is a standard communication interface between a client and conforms to the constraints of REST architectural style. In a REST API, HTTP messages are stateless and use JSON data format.")]),e("li",null,[e("a",h,[n("HTTP messages"),a(s)]),n(": an http message is a textual message that contains different parts and can be either a request or a response. A request is the HTTP message that the client sends to the server and response is the HTTP message that the server sends to the client in reaction to the request. Both requests and responses have a part called a "),m,n(". In rest APIs, the body is generally formatted in JSON.")]),e("li",null,[e("a",f,[n("Codable"),a(s)]),n(": it is a type that can convert itself into and out of an external representation. It is equivalent to "),k,n(" in Java. This type is helpful if we want to convert an object into an out of a JSON string.")]),b,e("li",null,[e("a",v,[n("URLSession"),a(s)]),n(": The official iOS HTTP client which is part of the Foundation library. This library is not part of the Swift standard library but there is an implementation for non-Apple platforms which is called "),e("a",_,[n("swift-corelibs-foundation"),a(s)]),n(".")])]),g,e("p",null,[n("This PW relies on the "),e("a",w,[n("excellent tutorial from hackingwithswift"),a(s)]),n(". It guides you on how to fetch JSON data from "),e("a",q,[n("iTunes's API"),a(s)]),n(". Please find and excerpt of the response body below.")]),T])}const x=o(p,[["render",y],["__file","index.html.vue"]]),E=JSON.parse('{"path":"/api-communication/","title":"Communicate with a REST API","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Some useful concepts","slug":"some-useful-concepts","link":"#some-useful-concepts","children":[]},{"level":2,"title":"PW: call a REST API","slug":"pw-call-a-rest-api","link":"#pw-call-a-rest-api","children":[]}],"git":{"updatedTime":1660750594000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":2}]},"filePathRelative":"api-communication/README.md"}');export{x as comp,E as data}; diff --git a/assets/index.html-DKuyFSlS.js b/assets/index.html-DKuyFSlS.js new file mode 100644 index 0000000..b1f6828 --- /dev/null +++ b/assets/index.html-DKuyFSlS.js @@ -0,0 +1,28 @@ +import{_ as o,r as i,o as l,c,a as n,b as a,d as e,e as t}from"./app-Bbun9eEO.js";const p="/ios-training/assets/swftui-playground-MsNlfoqb.png",r="/ios-training/assets/hello-swiftui-DCdT_J-Q.png",u={},d=t('{ + "resultCount":50, + "results":[ + { + "wrapperType":"track", + "kind":"song", + "artistId":159260351, + "collectionId":1440913923, + "trackId":1440914010, + "artistName":"Taylor Swift", + "collectionName":"Taylor Swift (Bonus Track Version)", + "trackName":"Our Song", + "collectionCensoredName":"Taylor Swift (Bonus Track Version)", + } + ] +} +
UI development
Estimated time
1/2 day
Apple provides two official UI frameworks : UIKit and SwiftUI.
UIKit is the originally used framework for UI development. It relies on defining the UI in a separate file (storyboard or xib) and the behavior in a swift file. In 2019, Apple release the first version of SwiftUI.
The remainder of this training focuses on SwiftUI.
SwiftUI
SwiftUI brings a new approach to build UIs that we can summarize as follows:
',8),m={href:"https://developer.apple.com/documentation/swiftui",target:"_blank",rel:"noopener noreferrer"},f=n("h2",{id:"prerequisites",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#prerequisites"},[n("span",null,"Prerequisites")])],-1),h={href:"https://www.apple.com/swift/playgrounds/",target:"_blank",rel:"noopener noreferrer"},k={href:"https://swiftui-playground.kishikawakatsumi.com/",target:"_blank",rel:"noopener noreferrer"},v=n("figure",null,[n("img",{src:p,alt:"SwiftUI web playground",tabindex:"0",loading:"lazy"}),n("figcaption",null,"SwiftUI web playground")],-1),w={href:"https://github.com/compnerd/swift-win32",target:"_blank",rel:"noopener noreferrer"},b=t(`
- All the UI is defined in Swift code. Neither Storyboards nor xibs are needed anymore.
- The UI is defined in a declarative style.
- States and bindings allow to hold the app data. The app UI updates automatically when these data change.
- UI elements are structs that conform to the View protocol.
- Complex views can be defined by breaking them into smaller views. This is called view composition.
- The modifier technique is used to apply modifications to a view. A modifier returns a new view each time.
Anatomy of a basic view
The following code shows a sample view.
struct ContentView: View { + var body: some View { + VStack { + Text("Hello SwiftUI") + .font(.largeTitle) + .foregroundColor(.blue) + .padding() + Button(action: {}) { + HStack { + Image(systemName: "suit.heart.fill") + .foregroundColor(.red) + Text("I am a button") + .font(.headline) + .foregroundColor(.white) + } + .padding(12) + .background(Color.orange) + .cornerRadius(8) + } + } + } +} +
As noted earlier, SwiftUI views are structs that conforms to the View protocol. This protocol defined a computed property that returns a View as an opaque type.
The body of the view has a
VStack
as its root element. AVStack
is a container view that arrange its direct children vertically (on a column). The first child is aText
view and its second child is aButton
.The
Text
view chains calls to some methods that we call modifiers. They allow to do anything that we want to the view that called it and they return a newView
instance. This means that we can apply another modifier to the result of a modifier and so on (this is called chaining). This allows modifiers to have a declarative syntax that makes the code easy to understand. SwiftUI provides built-in modifiers and allows us to create custom ones. Can you match all the modifiers used in the code and their effects ?The modifiers used are:
font(...) +foregroundColor(...) +padding(...) +background(...) +cornerRadius(...) +
The button has no action, meaning that is does nothing on click and its content is defined as an
HStack
. AnHStack
is a container view that arrange its direct children horizontally (on a row). The stack contains an image and a button.The view renders as illustrated by the image below.
Let's do more SwiftUI.
A summary of important concepts
@State
: Single source of truth of a view and should not be shared with other views.@Binding
: allows to pass a reference of a state to a child view using$state
.@EnvironmentObject
: Allows to globally share data between views. An@EnvironmentObject
conforms to theObservableObject
protocol and its properties have the@Published
property wrapper.@ObservedObject
: Allows to observe changes in an object that conforms to theObservableObject
protocol.PW: complete some official SwiftUI tutorials
',14),g={href:"https://developer.apple.com/tutorials/swiftui",target:"_blank",rel:"noopener noreferrer"},_=n("p",null,"Please cover these tutorials to get a good grasp of SwiftUI.",-1),y={href:"https://developer.apple.com/tutorials/swiftui/creating-and-combining-views",target:"_blank",rel:"noopener noreferrer"},I={href:"https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation",target:"_blank",rel:"noopener noreferrer"},S={href:"https://developer.apple.com/tutorials/swiftui/handling-user-input",target:"_blank",rel:"noopener noreferrer"},U={href:"https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions",target:"_blank",rel:"noopener noreferrer"},x={href:"https://developer.apple.com/tutorials/swiftui/composing-complex-interfaces",target:"_blank",rel:"noopener noreferrer"},T={href:"https://developer.apple.com/tutorials/swiftui/working-with-ui-controls",target:"_blank",rel:"noopener noreferrer"};function A(q,V){const s=i("ExternalLinkIcon");return l(),c("div",null,[d,n("p",null,[a("The "),n("a",m,[a("official documentation of SwiftUI is available here"),e(s)]),a(".")]),f,n("p",null,[a("It is recommended to use Xcode to learn and create SwiftUI apps. For simple apps, we can use the "),n("a",h,[a("Swift Playgrounds"),e(s)]),a(" app. There is a "),n("a",k,[a("web playground"),e(s)]),a(" that can be exceptionally used. You can see a screenshot of the tool below.")]),v,n("p",null,[a("Another promising alternative to watch is "),n("a",w,[a("compnerd's windows port"),e(s)]),a(" of UIKit and SwiftUI. So, if you can have a recent version Xcode running, this is be the best IDE for SwiftUI development.")]),b,n("p",null,[a("Apple provides a comprehensive "),n("a",g,[a("SwiftUI tutorial"),e(s)]),a(" that covers most of the basic use cases such as creating views and handling inputs, animations and transitions.")]),_,n("ul",null,[n("li",null,[a("Basic layout "),n("ul",null,[n("li",null,[n("a",y,[a("Creating and combining views"),e(s)]),a(" (40 min)")]),n("li",null,[n("a",I,[a("Building lists and navigation"),e(s)]),a(" (35 min)")]),n("li",null,[n("a",S,[a("Handling user input"),e(s)]),a(" (20 min)")])])]),n("li",null,[a("Animations and complex layouts "),n("ul",null,[n("li",null,[n("a",U,[a("Animating views and transitions"),e(s)]),a(" (20 min)")]),n("li",null,[n("a",x,[a("Composing complex interfaces"),e(s)]),a(" (20 min)")]),n("li",null,[n("a",T,[a("Working with UI controls"),e(s)]),a(" (25 min)")])])])])])}const B=o(u,[["render",A],["__file","index.html.vue"]]),E=JSON.parse('{"path":"/ui-development/","title":"UI development","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"SwiftUI","slug":"swiftui","link":"#swiftui","children":[]},{"level":2,"title":"Prerequisites","slug":"prerequisites","link":"#prerequisites","children":[]},{"level":2,"title":"Anatomy of a basic view","slug":"anatomy-of-a-basic-view","link":"#anatomy-of-a-basic-view","children":[]},{"level":2,"title":"A summary of important concepts","slug":"a-summary-of-important-concepts","link":"#a-summary-of-important-concepts","children":[]},{"level":2,"title":"PW: complete some official SwiftUI tutorials","slug":"pw-complete-some-official-swiftui-tutorials","link":"#pw-complete-some-official-swiftui-tutorials","children":[]}],"git":{"updatedTime":1719475184000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":5},{"name":"yostane","email":"1958676+yostane@users.noreply.github.com","commits":1}]},"filePathRelative":"ui-development/README.md"}');export{B as comp,E as data}; diff --git a/assets/index.html-DOlN3MeL.js b/assets/index.html-DOlN3MeL.js new file mode 100644 index 0000000..28abb77 --- /dev/null +++ b/assets/index.html-DOlN3MeL.js @@ -0,0 +1 @@ +import{_ as i,r,o as s,c as l,a as e,b as t,d as a,e as o}from"./app-Bbun9eEO.js";const c="/ios-training/assets/swiftui-framework-wwdc-OMsLBsB_.jpg",p="/ios-training/assets/xcodes-DaBHvxPG.png",d="/ios-training/assets/xcode-new-project-BgcBHlMK.png",h={},f=o('Presentation
Welcompe the world of iOS development
iOS development consists of developing applications that can target mainly the iPhone and iPad but also macOS, iWatch and Apple TV. There are many ways to achieve this:
- Using the official frameworks and tools provided by Apple
- Using 3rd party frameworks and tools such as Capacitor, MAUI and Flutter
This training focuses on iOS development using the official tools and frameworks proposed by Apple. Our development stack will consist of the following items:
- Programming language: Swift
- UI Framework: SwiftUI
- IDEs: Xcode and Swift Playgrounds
In addition to that, It is also possible to leverage the Swift language (without SwiftUI) in order to develop console apps and servers on Window, Linux, macOS.
History
The early days of iOS development used the Objective-C language, the UIKit UI Framework and -the good old- Xcode. This ecosystem was basic but quite powerful and allowed to develop amazing apps. The continuous updates from Apple improved the developer experience. For example, memory management became automatic (thanks to ARC) and the layout system became capable of adapting to different screen sizes.
In WWDC 2014, Apple announced the Swift language as an Open Source modern replacement to Objective-C. Following that, apple announced during the next WWDC SwiftUI as the replacement for UIKit.
As of 2021, the majority of new iOS projects use Swift and SwiftUI with UIKit as a fallback for the UI aspects.
Getting started
',13),u=e("a",{href:""},"Xcodes",-1),g={href:"https://apps.apple.com/us/app/swift-playgrounds/id1496833156?mt=12",target:"_blank",rel:"noopener noreferrer"},m=e("ul",null,[e("li",null,"Xcodes is a tools that downloads and manages the different versions of Xcode.")],-1),w=e("figure",null,[e("img",{src:p,alt:"xcodes",tabindex:"0",loading:"lazy"}),e("figcaption",null,"xcodes")],-1),_={href:"https://swiftforwindows.github.io/",target:"_blank",rel:"noopener noreferrer"},y={href:"https://swift.org/download/#releases",target:"_blank",rel:"noopener noreferrer"},b=e("li",null,"You can use VSCode or Fleet as an IDE.",-1),k={href:"https://github.com/compnerd/swift-win32",target:"_blank",rel:"noopener noreferrer"},S=e("li",null,[t("Open a terminal and run the following command to check if Swift is installed "),e("code",null,"swift --version"),t(".")],-1),v=o('Create a CLI app with swift CLI
- Open a terminal in an empty folder
mkdir MyCLI
and thencd MyCLI
- Create the project with
swift package init --name MyCLI --type executable
- and run it with
swift run
. It should print -> Hello worldCreate an App using Xcode or Swift playgrounds
- On Xcode, create a either a Project or Playground and run it.
Swift project managers
',6),x={href:"https://swift.org/package-manager/",target:"_blank",rel:"noopener noreferrer"},I=e("li",null,"Xcode projects are projects fully managed by Xcode (.xcodeproj)",-1),O={href:"https://github.com/Carthage/Carthage",target:"_blank",rel:"noopener noreferrer"},C={href:"https://cocoapods.org/",target:"_blank",rel:"noopener noreferrer"},j=e("p",null,"In this training, we'll use swiftpm and Xcode.",-1),U=e("h2",{id:"links-and-references",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#links-and-references"},[e("span",null,"Links and references")])],-1),A={href:"https://9to5mac.com/2019/06/03/apple-announces-swiftui-a-modern-declarative-user-interface-framework-for-apple-platforms/",target:"_blank",rel:"noopener noreferrer"},L={href:"https://www.apple.com/newsroom/2015/12/03Apple-Releases-Swift-as-Open-Source/",target:"_blank",rel:"noopener noreferrer"},P={href:"https://www.sutori.com/en/story/the-history-of-ios-app-development--hzFfwkD2KYLaa5WrxsrUFGMh",target:"_blank",rel:"noopener noreferrer"},W={href:"https://www.timetoast.com/timelines/history-of-ios",target:"_blank",rel:"noopener noreferrer"};function X(T,B){const n=r("ExternalLinkIcon");return s(),l("div",null,[f,e("ul",null,[e("li",null,[t("On macOS, install "),u,t(" and "),e("a",g,[t("Swift Playgrounds"),a(n)]),t(". "),m])]),w,e("ul",null,[e("li",null,[t("For windows and Linux users you can install "),e("a",_,[t("Swift for Windows"),a(n)]),t(" or "),e("a",y,[t("Swift for Ubuntu"),a(n)]),t(". This will allow you to run Swift code on your machine. "),e("ul",null,[b,e("li",null,[t("Unfortunately, SwiftUI development either challenging or not supported. (on Windows you can try to use "),e("a",k,[t("swift-win32"),a(n)]),t(")")])])]),S]),v,e("ul",null,[e("li",null,[t("Official tools: "),e("ul",null,[e("li",null,[e("a",x,[t("Swift Package Manager"),a(n)]),t(" (also called swiftpm) is the official tool to manage Swift projects. It is platform and IDE agnostic.")]),I])]),e("li",null,[t("3rd party tools: "),e("ul",null,[e("li",null,[e("a",O,[t("Carthage"),a(n)])]),e("li",null,[e("a",C,[t("CocoaPods"),a(n)])])])])]),j,U,e("ul",null,[e("li",null,[e("a",A,[t("Apple announces SwiftUI, a modern declarative user interface framework for Apple platforms"),a(n)])]),e("li",null,[e("a",L,[t("Apple Releases Swift as Open Source"),a(n)])]),e("li",null,[e("a",P,[t("The History of iOS App Development (from iOS 1 to 8)"),a(n)])]),e("li",null,[e("a",W,[t("History of iOS from 2007 to 2018"),a(n)])])])])}const F=i(h,[["render",X],["__file","index.html.vue"]]),M=JSON.parse('{"path":"/presentation/","title":"Presentation","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Welcompe the world of iOS development","slug":"welcompe-the-world-of-ios-development","link":"#welcompe-the-world-of-ios-development","children":[]},{"level":2,"title":"History","slug":"history","link":"#history","children":[]},{"level":2,"title":"Getting started","slug":"getting-started","link":"#getting-started","children":[{"level":3,"title":"Create a CLI app with swift CLI","slug":"create-a-cli-app-with-swift-cli","link":"#create-a-cli-app-with-swift-cli","children":[]},{"level":3,"title":"Create an App using Xcode or Swift playgrounds","slug":"create-an-app-using-xcode-or-swift-playgrounds","link":"#create-an-app-using-xcode-or-swift-playgrounds","children":[]}]},{"level":2,"title":"Swift project managers","slug":"swift-project-managers","link":"#swift-project-managers","children":[]},{"level":2,"title":"Links and references","slug":"links-and-references","link":"#links-and-references","children":[]}],"git":{"updatedTime":1719246851000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":5},{"name":"yassine benabbas (a527524)","email":"yassine.benabbas@worldline.com","commits":2},{"name":"yostane","email":"1958676+yostane@users.noreply.github.com","commits":1}]},"filePathRelative":"presentation/README.md"}');export{F as comp,M as data}; diff --git a/assets/index.html-z4YYCP43.js b/assets/index.html-z4YYCP43.js new file mode 100644 index 0000000..b739a10 --- /dev/null +++ b/assets/index.html-z4YYCP43.js @@ -0,0 +1,84 @@ +import{_ as i,r as o,o as p,c as l,a as n,b as s,d as t,e}from"./app-Bbun9eEO.js";const c={},u=n("h1",{id:"mini-project",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#mini-project"},[n("span",null,"Mini project")])],-1),r=n("p",null,"The final chapter of this training will ask you to create a SwiftUI app from scratch.",-1),d=n("h2",{id:"requirements",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#requirements"},[n("span",null,"Requirements")])],-1),k=n("p",null,"The app consists of a movie explorer app with the following features:",-1),v=n("li",null,"Search for movies by title.",-1),m=n("li",null,"View the details of the selected movie.",-1),h=n("li",null,"The app requires the user to be logged in.",-1),b=n("li",null,"The app allows a new user to register.",-1),g=n("li",null,"The movie list screen allows to logout from the app.",-1),w=n("li",null,"The app remembers the logged in user after a restart.",-1),f={href:"https://movie-api-ybwl.koyeb.app/api-docs/",target:"_blank",rel:"noopener noreferrer"},_=n("ul",null,[n("li",null,[s("The "),n("strong",null,"/movies/search"),s(" endpoint requires to pass the token retrieved from endpoint "),n("strong",null,"/user/login"),s(" or "),n("strong",null,"user/register"),s(" in this header: "),n("code",null,"Authorization: Bearer \\(userResponse.token)")])],-1),y=n("li",null,"(Optional) The result of previous queries is locally cached.",-1),q=n("li",null,"(Optional) Add movie to local favorites ⭐️",-1),T={href:"https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-and-remove-views-with-a-transition",target:"_blank",rel:"noopener noreferrer"},S=n("p",null,"A preview of the app can be seen here.",-1),x=n("iframe",{width:"720",height:"576",src:"https://www.youtube.com/embed/vh5AlaGK0Eo",title:"YouTube video player",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1),I=e(`Hints
- There are many techniques to handle the flow from the login view to the movie list view. On of them is to rely on a logged state. The following gives an overview how it looks like.
struct ContentView: View { + @State var loggedIn: false + + var body: some View { + if loggedIn { + MovieListView() + } else { + // The LoginView takes a callback that is called when the login succeeds + LoginView { newLoggedIn in + loggedIn = newLoggedIn + } + } + } +} +
- In the login view, use an enum to track the state of the login operation so that you can disable the login button when a request is running.
enum LoginState { + case neutral, loading, success, failure +} +struct LoginView: View { + @State private var loginState: LoginState = .neutral + // other code +} +
- Use a Task object to run async code.
Button("Login") { + loginState = .loading + Task { + if await login() { + onLoginSuccess(true) + } + } +} +
`,8),L={href:"https://github.com/Tunous/DebouncedOnChange",target:"_blank",rel:"noopener noreferrer"},V=n("li",null,[s("To generate the initial code for a preview, open a view and then use the Xcode feature "),n("em",null,"Editor -> Create preview")],-1),A=n("li",null,[s("The List view requires that you specify an "),n("code",null,"id"),s(" field "),n("code",null,"List(movies, id: \\.title)"),s(" or that the items conform to Identifiable protocol")],-1),C=n("li",null,[s("If you can't add SwiftPM packages from Xcode, add them by editing the "),n("em",null,"package.swift"),s(" file by hand. Here is an example below.")],-1),O=e(`Swift Concurrency crashes on Swift Playground
Do not use the Swift Playground app to run you app as it does not work well with SwiftUI + Swift Concurrency (async, await and Task). Instead, you can create an Xcode project of type Playground to combine the power of Xcode and the simplicity of Playground projects.
`,1);function D(P,j){const a=o("ExternalLinkIcon");return p(),l("div",null,[u,r,d,k,n("ul",null,[v,m,h,b,g,w,n("li",null,[s("The app uses "),n("a",f,[s("this API"),t(a)]),s(" for the authenticating and searching for movies. "),_]),y,q,n("li",null,[s("(Optional) Animate the transition between the login view and the movie list view ("),n("a",T,[s("tutorial"),t(a)]),s(").")])]),S,x,I,n("ul",null,[n("li",null,[s("Use "),n("a",L,[s("DebouncedOnChange"),t(a)]),s(" Swift package to optimize search.")]),V,A,C]),O])}const N=i(c,[["render",D],["__file","index.html.vue"]]),E=JSON.parse('{"path":"/mini-project/","title":"Mini project","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"Requirements","slug":"requirements","link":"#requirements","children":[]},{"level":2,"title":"Hints","slug":"hints","link":"#hints","children":[]}],"git":{"updatedTime":1701187541000,"contributors":[{"name":"Yassine Benabbas","email":"yassine.benabbas@worldline.com","commits":4},{"name":"yostane","email":"1958676+yostane@users.noreply.github.com","commits":1}]},"filePathRelative":"mini-project/README.md"}');export{N as comp,E as data}; diff --git a/assets/style-DGf4msjw.css b/assets/style-DGf4msjw.css new file mode 100644 index 0000000..635f241 --- /dev/null +++ b/assets/style-DGf4msjw.css @@ -0,0 +1 @@ +.vp-back-to-top-button{position:fixed!important;bottom:4rem;inset-inline-end:1rem;z-index:100;width:48px;height:48px;padding:8px;border-width:0;border-radius:50%;background:var(--back-to-top-bg-color);color:var(--back-to-top-color);box-shadow:2px 2px 10px 4px var(--back-to-top-shadow);cursor:pointer}@media (max-width: 959px){.vp-back-to-top-button{transform:scale(.8);transform-origin:100% 100%}}@media print{.vp-back-to-top-button{display:none}}.vp-back-to-top-button:hover{color:var(--back-to-top-color-hover)}.vp-back-to-top-button .back-to-top-icon{overflow:hidden;width:100%;height:100%;background:currentcolor;border-radius:50%;-webkit-mask-image:var(--back-to-top-icon);mask-image:var(--back-to-top-icon);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:cover;mask-size:cover}.vp-scroll-progress{position:absolute;right:-2px;bottom:-2px;width:52px;height:52px}.vp-scroll-progress svg{width:100%;height:100%}.vp-scroll-progress circle{opacity:.9;transform:rotate(-90deg);transform-origin:50% 50%}.back-to-top-enter-active,.back-to-top-leave-active{transition:opacity .3s}.back-to-top-enter-from,.back-to-top-leave-to{opacity:0}:root{--back-to-top-z-index: 5;--back-to-top-icon: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201024%201024'%3e%3cpath%20d='M512%20843.2c-36.2%200-66.4-13.6-85.8-21.8-10.8-4.6-22.6%203.6-21.8%2015.2l7%20102c.4%206.2%207.6%209.4%2012.6%205.6l29-22c3.6-2.8%209-1.8%2011.4%202l41%2064.2c3%204.8%2010.2%204.8%2013.2%200l41-64.2c2.4-3.8%207.8-4.8%2011.4-2l29%2022c5%203.8%2012.2.6%2012.6-5.6l7-102c.8-11.6-11-20-21.8-15.2-19.6%208.2-49.6%2021.8-85.8%2021.8'/%3e%3cpath%20d='m795.4%20586.2-96-98.2C699.4%20172%20513%2032%20513%2032S324.8%20172%20324.8%20488l-96%2098.2c-3.6%203.6-5.2%209-4.4%2014.2L261.2%20824c1.8%2011.4%2014.2%2017%2023.6%2010.8L419%20744s41.4%2040%2094.2%2040%2092.2-40%2092.2-40l134.2%2090.8c9.2%206.2%2021.6.6%2023.6-10.8l37-223.8c.4-5.2-1.2-10.4-4.8-14M513%20384c-34%200-61.4-28.6-61.4-64s27.6-64%2061.4-64c34%200%2061.4%2028.6%2061.4%2064S547%20384%20513%20384'/%3e%3c/svg%3e");--back-to-top-bg-color: #fff;--back-to-top-color: #3eaf7c;--back-to-top-color-hover: #71cda3;--back-to-top-shadow: rgb(0 0 0 / 20%)}div[class*=language-]:hover:before{display:none}div[class*=language-]:hover .vp-copy-code-button{opacity:1}.vp-copy-code-button{position:absolute;top:.5em;right:.5em;z-index:5;width:2.5rem;height:2.5rem;padding:0;border-width:0;border-radius:.5rem;background:transparent;outline:none;opacity:0;cursor:pointer;transition:opacity .4s}@media print{.vp-copy-code-button{display:none}}.vp-copy-code-button:focus,.vp-copy-code-button.copied{opacity:1}.vp-copy-code-button:hover,.vp-copy-code-button.copied{background:var(--copy-code-hover)}.vp-copy-code-button.copied .vp-copy-icon{-webkit-mask-image:var(--code-copied-icon);mask-image:var(--code-copied-icon)}.vp-copy-code-button.copied:after{content:attr(data-copied);position:absolute;top:0;right:calc(100% + .25rem);display:block;height:1.25rem;padding:.625rem;border-radius:.5rem;background:var(--copy-code-hover);color:var(--copy-code-color);font-weight:500;line-height:1.25rem;white-space:nowrap}.vp-copy-icon{width:1.25rem;height:1.25rem;padding:.625rem;background:currentcolor;color:var(--copy-code-color);font-size:1.25rem;-webkit-mask-image:var(--code-copy-icon);mask-image:var(--code-copy-icon);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:1em;mask-size:1em}:root{--code-copy-icon: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20height='20'%20width='20'%20stroke='rgba(128,128,128,1)'%20stroke-width='2'%3e%3cpath%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M9%205H7a2%202%200%200%200-2%202v12a2%202%200%200%200%202%202h10a2%202%200%200%200%202-2V7a2%202%200%200%200-2-2h-2M9%205a2%202%200%200%200%202%202h2a2%202%200%200%200%202-2M9%205a2%202%200%200%201%202-2h2a2%202%200%200%201%202%202'%20/%3e%3c/svg%3e");--code-copied-icon: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20height='20'%20width='20'%20stroke='rgba(128,128,128,1)'%20stroke-width='2'%3e%3cpath%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M9%205H7a2%202%200%200%200-2%202v12a2%202%200%200%200%202%202h10a2%202%200%200%200%202-2V7a2%202%200%200%200-2-2h-2M9%205a2%202%200%200%200%202%202h2a2%202%200%200%200%202-2M9%205a2%202%200%200%201%202-2h2a2%202%200%200%201%202%202m-6%209%202%202%204-4'%20/%3e%3c/svg%3e");--copy-code-color: #9e9e9e;--copy-code-hover: rgb(0 0 0 / 50%)}:root{--external-link-icon-color: #aaa}.external-link-icon{position:relative;display:inline-block;color:var(--external-link-icon-color);vertical-align:middle;top:-1px}@media print{.external-link-icon{display:none}}.external-link-icon-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--nprogress-color: #29d;--nprogress-z-index: 1031}#nprogress{pointer-events:none}#nprogress .bar{background:var(--nprogress-color);position:fixed;z-index:var(--nprogress-z-index);top:0;left:0;width:100%;height:2px}.vp-page-meta{max-width:var(--content-width);margin:0 auto;padding:.75rem 2.5rem;display:flex;flex-wrap:wrap;justify-content:space-between;overflow:auto}@media (max-width: 959px){.vp-page-meta{padding:2rem}}@media (max-width: 419px){.vp-page-meta{padding:1.5rem}}@media print{.vp-page-meta{margin:0!important;padding-inline:0!important}}@media (max-width: 719px){.vp-page-meta{display:block}}.vp-page-meta .vp-meta-item{flex-grow:1}.vp-page-meta .vp-meta-item .vp-meta-label{font-weight:500}.vp-page-meta .vp-meta-item .vp-meta-label:not(a){color:var(--c-text-lighter)}.vp-page-meta .vp-meta-item .vp-meta-info{color:var(--c-text-quote);font-weight:400}.vp-page-meta .git-info{text-align:end}.vp-page-meta .edit-link{margin-top:.25rem;margin-bottom:.25rem;margin-inline-end:.5rem;font-size:14px}@media print{.vp-page-meta .edit-link{display:none}}.vp-page-meta .edit-link .icon{position:relative;bottom:-.125em;width:1em;height:1em;margin-inline-end:.25em}.vp-page-meta .last-updated,.vp-page-meta .contributors{margin-top:.25rem;margin-bottom:.25rem;font-size:14px}@media (max-width: 719px){.vp-page-meta .last-updated,.vp-page-meta .contributors{font-size:13px;text-align:start}}.vp-page-nav{display:flex;flex-wrap:wrap;max-width:var(--content-width, 740px);min-height:2rem;margin-inline:auto;margin-top:0;padding-block:.5rem;padding-inline:2rem;border-top:1px solid var(--c-border);transition:border-top var(--t-color);padding-top:1rem;padding-bottom:0}@media (max-width: 959px){.vp-page-nav{padding-inline:1rem}}@media print{.vp-page-nav{display:none}}.vp-page-nav .route-link{display:inline-block;flex-grow:1;margin:.25rem;padding:.25rem .5rem;border:1px solid var(--c-border);border-radius:.25rem}.vp-page-nav .route-link:hover{background:var(--c-bg-light)}.vp-page-nav .route-link .hint{color:var(--c-text-quote);font-size:.875rem;line-height:2}.vp-page-nav .prev{text-align:start}.vp-page-nav .next{text-align:end}:root{--c-brand: #3eaf7c;--c-brand-light: #4abf8a;--c-bg: #ffffff;--c-bg-light: #f3f4f5;--c-bg-lighter: #eeeeee;--c-bg-dark: #ebebec;--c-bg-darker: #e6e6e6;--c-bg-navbar: var(--c-bg);--c-bg-sidebar: var(--c-bg);--c-bg-arrow: #cccccc;--c-text: #2c3e50;--c-text-accent: var(--c-brand);--c-text-light: #3a5169;--c-text-lighter: #4e6e8e;--c-text-lightest: #6a8bad;--c-text-quote: #999999;--c-border: #eaecef;--c-border-dark: #dfe2e5;--c-tip: #42b983;--c-tip-bg: var(--c-bg-light);--c-tip-title: var(--c-text);--c-tip-text: var(--c-text);--c-tip-text-accent: var(--c-text-accent);--c-warning: #ffc310;--c-warning-bg: #fffae3;--c-warning-bg-light: #fff3ba;--c-warning-bg-lighter: #fff0b0;--c-warning-border-dark: #f7dc91;--c-warning-details-bg: #fff5ca;--c-warning-title: #f1b300;--c-warning-text: #746000;--c-warning-text-accent: #edb100;--c-warning-text-light: #c1971c;--c-warning-text-quote: #ccab49;--c-danger: #f11e37;--c-danger-bg: #ffe0e0;--c-danger-bg-light: #ffcfde;--c-danger-bg-lighter: #ffc9c9;--c-danger-border-dark: #f1abab;--c-danger-details-bg: #ffd4d4;--c-danger-title: #ed1e2c;--c-danger-text: #660000;--c-danger-text-accent: #bd1a1a;--c-danger-text-light: #b5474d;--c-danger-text-quote: #c15b5b;--c-details-bg: #eeeeee;--c-badge-tip: var(--c-tip);--c-badge-warning: #ecc808;--c-badge-warning-text: var(--c-bg);--c-badge-danger: #dc2626;--c-badge-danger-text: var(--c-bg);--c-code-group-tab-title: rgba(255, 255, 255, .9);--c-code-group-tab-bg: var(--code-bg-color);--c-code-group-tab-outline: var(var(--c-code-group-tab-title));--c-code-group-tab-active-border: var(--c-brand);--t-color: .3s ease;--t-transform: .3s ease;--code-bg-color: #282c34;--code-hl-bg-color: rgba(0, 0, 0, .66);--code-ln-color: #9e9e9e;--code-ln-wrapper-width: 3.5rem;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;--font-family-code: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;--navbar-height: 3.6rem;--navbar-padding-v: .7rem;--navbar-padding-h: 1.5rem;--sidebar-width: 20rem;--sidebar-width-mobile: calc(var(--sidebar-width) * .82);--content-width: 740px;--homepage-width: 960px}.vp-back-to-top-button{--back-to-top-color: var(--c-brand);--back-to-top-color-hover: var(--c-brand-light);--back-to-top-bg-color: var(--c-bg)}.vp-catalog-wrapper{--catalog-bg-color: var(--c-bg);--catalog-bg-secondary-color: var(--c-bg-dark);--catalog-border-color: var(--c-border);--catalog-active-color: var(--c-brand);--catalog-hover-color: var(--c-brand-light)}.waline-wrapper{--waline-bg-color: var(--c-bg);--waline-bg-color-light: var(--c-bg-light);--waline-text-color: var(--c-color);--waline-border: 1px solid var(--c-border);--waline-border-color: var(--c-border);--waline-theme-color: var(--c-brand);--waline-active-color: var(--c-brand-light)}.DocSearch{--docsearch-primary-color: var(--c-brand);--docsearch-text-color: var(--c-text);--docsearch-highlight-color: var(--c-brand);--docsearch-muted-color: var(--c-text-quote);--docsearch-container-background: rgba(9, 10, 17, .8);--docsearch-modal-background: var(--c-bg-light);--docsearch-searchbox-background: var(--c-bg-lighter);--docsearch-searchbox-focus-background: var(--c-bg);--docsearch-searchbox-shadow: inset 0 0 0 2px var(--c-brand);--docsearch-hit-color: var(--c-text-light);--docsearch-hit-active-color: var(--c-bg);--docsearch-hit-background: var(--c-bg);--docsearch-hit-shadow: 0 1px 3px 0 var(--c-border-dark);--docsearch-footer-background: var(--c-bg)}.external-link-icon{--external-link-icon-color: var(--c-text-quote)}.medium-zoom-overlay{--medium-zoom-bg-color: var(--c-bg)}#nprogress{--nprogress-color: var(--c-brand)}body{--photo-swipe-bullet: var(--c-bg);--photo-swipe-bullet-active: var(--c-brand)}body{--pwa-text-color: var(--c-text);--pwa-bg-color: var(--c-bg);--pwa-border-color: var(--c-brand);--pwa-btn-text-color: var(--c-bg);--pwa-btn-bg-color: var(--c-brand);--pwa-btn-hover-bg-color: var(--c-brand-light)}.language-modal-mask{--redirect-bg-color: var(--c-bg);--redirect-bg-color-light: var(--c-bg-light);--redirect-bg-color-lighter: var(--c-bg-lighter);--redirect-text-color: var(--c-text);--redirect-primary-color: var(--c-brand);--redirect-primary-hover-color: var(--c-brand-light);--redirect-primary-text-color: var(--c-bg)}.search-box{--search-bg-color: var(--c-bg);--search-accent-color: var(--c-brand);--search-text-color: var(--c-text);--search-border-color: var(--c-border);--search-item-text-color: var(--c-text-lighter);--search-item-focus-bg-color: var(--c-bg-light)}html.dark{--c-brand: #3aa675;--c-brand-light: #349469;--c-bg: #22272e;--c-bg-light: #2b313a;--c-bg-lighter: #262c34;--c-bg-dark: #343b44;--c-bg-darker: #37404c;--c-text: #adbac7;--c-text-light: #96a7b7;--c-text-lighter: #8b9eb0;--c-text-lightest: #8094a8;--c-border: #3e4c5a;--c-border-dark: #34404c;--c-tip: #318a62;--c-warning: #e0ad15;--c-warning-bg: #2d2f2d;--c-warning-bg-light: #423e2a;--c-warning-bg-lighter: #44442f;--c-warning-border-dark: #957c35;--c-warning-details-bg: #39392d;--c-warning-title: #fdca31;--c-warning-text: #d8d96d;--c-warning-text-accent: #ffbf00;--c-warning-text-light: #ddb84b;--c-warning-text-quote: #ccab49;--c-danger: #fc1e38;--c-danger-bg: #39232c;--c-danger-bg-light: #4b2b35;--c-danger-bg-lighter: #553040;--c-danger-border-dark: #a25151;--c-danger-details-bg: #482936;--c-danger-title: #fc2d3b;--c-danger-text: #ea9ca0;--c-danger-text-accent: #fd3636;--c-danger-text-light: #d9777c;--c-danger-text-quote: #d56b6b;--c-details-bg: #323843;--c-badge-warning: var(--c-warning);--c-badge-warning-text: #3c2e05;--c-badge-danger: var(--c-danger);--c-badge-danger-text: #401416;--code-hl-bg-color: #363b46}html.dark .DocSearch{--docsearch-logo-color: var(--c-text);--docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;--docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgba(3, 4, 9, .3);--docsearch-key-gradient: linear-gradient(-225deg, #444950, #1c1e21);--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2)}html.dark body{--pwa-shadow-color: rgb(0 0 0 / 30%);--pwa-content-color: #ccc;--pwa-content-light-color: #999}html,body{padding:0;margin:0;background-color:var(--c-bg);transition:background-color var(--t-color)}html.dark{color-scheme:dark}html{font-size:16px}body{font-family:var(--font-family);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:1rem;color:var(--c-text)}a{font-weight:500;color:var(--c-text-accent);text-decoration:none;overflow-wrap:break-word}p a code{font-weight:400;color:var(--c-text-accent)}kbd{font-family:var(--font-family-code);color:var(--c-text);background:var(--c-bg-lighter);border:solid .15rem var(--c-border-dark);border-bottom:solid .25rem var(--c-border-dark);border-radius:.15rem;padding:0 .15em}code{font-family:var(--font-family-code);color:var(--c-text-lighter);padding:.25rem .5rem;margin:0;font-size:.85em;background-color:var(--c-bg-light);border-radius:3px;overflow-wrap:break-word;transition:background-color var(--t-color)}blockquote{font-size:1rem;color:var(--c-text-quote);border-left:.2rem solid var(--c-border-dark);margin:1rem 0;padding:.25rem 0 .25rem 1rem;overflow-wrap:break-word}blockquote>p{margin:0}ul,ol{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25;overflow-wrap:break-word}h1:focus-visible,h2:focus-visible,h3:focus-visible,h4:focus-visible,h5:focus-visible,h6:focus-visible{outline:none}h1 .header-anchor,h2 .header-anchor,h3 .header-anchor,h4 .header-anchor,h5 .header-anchor,h6 .header-anchor{color:inherit;text-decoration:none;position:relative}h1 .header-anchor:hover:before,h2 .header-anchor:hover:before,h3 .header-anchor:hover:before,h4 .header-anchor:hover:before,h5 .header-anchor:hover:before,h6 .header-anchor:hover:before{font-size:.8em;content:"¶";position:absolute;left:-.75em;color:var(--c-brand)}h1 .header-anchor:focus-visible,h2 .header-anchor:focus-visible,h3 .header-anchor:focus-visible,h4 .header-anchor:focus-visible,h5 .header-anchor:focus-visible,h6 .header-anchor:focus-visible{outline:none}h1 .header-anchor:focus-visible:before,h2 .header-anchor:focus-visible:before,h3 .header-anchor:focus-visible:before,h4 .header-anchor:focus-visible:before,h5 .header-anchor:focus-visible:before,h6 .header-anchor:focus-visible:before{content:"¶";position:absolute;left:-.75em;color:var(--c-brand);outline:auto}h1{font-size:2.2rem}h2{font-size:1.65rem;padding-bottom:.3rem;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color)}h3{font-size:1.35rem}h4{font-size:1.15rem}h5{font-size:1.05rem}h6{font-size:1rem}@media print{a[href^="http://"]:after,a[href^="https://"]:after{content:" (" attr(href) ") "}}p,ul,ol{line-height:1.7;overflow-wrap:break-word}hr{border:0;border-top:1px solid var(--c-border)}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto;transition:border-color var(--t-color)}tr{border-top:1px solid var(--c-border-dark);transition:border-color var(--t-color)}tr:nth-child(2n){background-color:var(--c-bg-light);transition:background-color var(--t-color)}tr:nth-child(2n) code{background-color:var(--c-bg-dark)}th,td{padding:.6em 1em;border:1px solid var(--c-border-dark);transition:border-color var(--t-color)}.arrow{display:inline-block;vertical-align:middle;width:1em;height:1em;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(0,0,0,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;line-height:normal;transition:all .3s}html.dark .arrow{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(255,255,255,0.5)' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E")}.arrow.down{transform:rotate(180deg)}.arrow.right{transform:rotate(90deg)}.arrow.left{transform:rotate(-90deg)}.badge{display:inline-block;font-size:14px;font-weight:600;height:18px;line-height:18px;border-radius:3px;padding:0 6px;color:var(--c-bg);vertical-align:top;transition:color var(--t-color),background-color var(--t-color)}.badge.tip{background-color:var(--c-badge-tip)}.badge.warning{background-color:var(--c-badge-warning);color:var(--c-badge-warning-text)}.badge.danger{background-color:var(--c-badge-danger);color:var(--c-badge-danger-text)}.badge+.badge{margin-left:5px}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:var(--font-family-code);font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata{color:#999}.token.punctuation{color:#ccc}.token.tag,.token.attr-name,.token.namespace,.token.deleted{color:#ec5975}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}.token.important,.token.bold{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:#3eaf7c}.theme-default-content pre,.theme-default-content pre[class*=language-]{line-height:1.375;padding:1.3rem 1.5rem;margin:.85rem 0;border-radius:6px;overflow:auto}.theme-default-content pre code,.theme-default-content pre[class*=language-] code{color:#fff;padding:0;background-color:transparent!important;border-radius:0;overflow-wrap:unset;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.theme-default-content .line-number{font-family:var(--font-family-code)}div[class*=language-]{position:relative;background-color:var(--code-bg-color);border-radius:6px}div[class*=language-]:before{content:attr(data-title);position:absolute;z-index:3;top:.8em;right:1em;font-size:.75rem;color:var(--code-ln-color)}div[class*=language-] pre,div[class*=language-] pre[class*=language-]{background:transparent!important;position:relative;z-index:1}div[class*=language-] .highlight-lines{-webkit-user-select:none;-moz-user-select:none;user-select:none;padding-top:1.3rem;position:absolute;top:0;left:0;width:100%;line-height:1.375}div[class*=language-] .highlight-lines .highlight-line{background-color:var(--code-hl-bg-color)}div[class*=language-]:not(.line-numbers-mode) .line-numbers{display:none}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line{position:relative}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line:before{content:" ";position:absolute;z-index:2;left:0;top:0;display:block;width:var(--code-ln-wrapper-width);height:100%}div[class*=language-].line-numbers-mode pre{margin-left:var(--code-ln-wrapper-width);padding-left:1rem;vertical-align:middle}div[class*=language-].line-numbers-mode .line-numbers{position:absolute;top:0;width:var(--code-ln-wrapper-width);text-align:center;color:var(--code-ln-color);padding-top:1.25rem;line-height:1.375;counter-reset:line-number}div[class*=language-].line-numbers-mode .line-numbers .line-number{position:relative;z-index:3;-webkit-user-select:none;-moz-user-select:none;user-select:none;height:1.375em}div[class*=language-].line-numbers-mode .line-numbers .line-number:before{counter-increment:line-number;content:counter(line-number);font-size:.85em}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;top:0;left:0;width:var(--code-ln-wrapper-width);height:100%;border-radius:6px 0 0 6px;border-right:1px solid var(--code-hl-bg-color)}@media (max-width: 419px){.theme-default-content div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}.code-group__nav{margin-top:.85rem;margin-bottom:calc(-1.7rem - 6px);padding-bottom:calc(1.7rem - 6px);padding-left:10px;padding-top:10px;border-top-left-radius:6px;border-top-right-radius:6px;background-color:var(--c-code-group-tab-bg)}.code-group__nav-tab{border:0;padding:5px;cursor:pointer;background-color:transparent;font-size:.85em;line-height:1.4;color:var(--c-code-group-tab-title);font-weight:600}.code-group__nav-tab:focus{outline:none}.code-group__nav-tab:focus-visible{outline:1px solid var(--c-code-group-tab-outline)}.code-group__nav-tab-active{border-bottom:var(--c-code-group-tab-active-border) 1px solid}@media (max-width: 419px){.code-group__nav{margin-left:-1.5rem;margin-right:-1.5rem;border-radius:0}}.code-group-item{display:none}.code-group-item__active{display:block}.code-group-item>pre{background-color:orange}.custom-container{transition:color var(--t-color),border-color var(--t-color),background-color var(--t-color)}.custom-container .custom-container-title{font-weight:600}.custom-container .custom-container-title:not(:only-child){margin-bottom:-.4rem}.custom-container.tip,.custom-container.warning,.custom-container.danger{padding:.1rem 1.5rem;border-left-width:.5rem;border-left-style:solid;margin:1rem 0}.custom-container.tip{border-color:var(--c-tip);background-color:var(--c-tip-bg);color:var(--c-tip-text)}.custom-container.tip .custom-container-title{color:var(--c-tip-title)}.custom-container.tip a{color:var(--c-tip-text-accent)}.custom-container.tip code{background-color:var(--c-bg-dark)}.custom-container.warning{border-color:var(--c-warning);background-color:var(--c-warning-bg);color:var(--c-warning-text)}.custom-container.warning .custom-container-title{color:var(--c-warning-title)}.custom-container.warning a{color:var(--c-warning-text-accent)}.custom-container.warning blockquote{border-left-color:var(--c-warning-border-dark);color:var(--c-warning-text-quote)}.custom-container.warning code{color:var(--c-warning-text-light);background-color:var(--c-warning-bg-light)}.custom-container.warning details{background-color:var(--c-warning-details-bg)}.custom-container.warning details code{background-color:var(--c-warning-bg-lighter)}.custom-container.warning .external-link-icon{--external-link-icon-color: var(--c-warning-text-quote)}.custom-container.danger{border-color:var(--c-danger);background-color:var(--c-danger-bg);color:var(--c-danger-text)}.custom-container.danger .custom-container-title{color:var(--c-danger-title)}.custom-container.danger a{color:var(--c-danger-text-accent)}.custom-container.danger blockquote{border-left-color:var(--c-danger-border-dark);color:var(--c-danger-text-quote)}.custom-container.danger code{color:var(--c-danger-text-light);background-color:var(--c-danger-bg-light)}.custom-container.danger details{background-color:var(--c-danger-details-bg)}.custom-container.danger details code{background-color:var(--c-danger-bg-lighter)}.custom-container.danger .external-link-icon{--external-link-icon-color: var(--c-danger-text-quote)}.custom-container.details{display:block;position:relative;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:var(--c-details-bg)}.custom-container.details code{background-color:var(--c-bg-darker)}.custom-container.details h4{margin-top:0}.custom-container.details figure:last-child,.custom-container.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-container.details summary{outline:none;cursor:pointer}.home{padding:var(--navbar-height) 2rem 0;max-width:var(--homepage-width);margin:0 auto;display:block}.home .hero{text-align:center}.home .hero img{max-width:100%;max-height:280px;display:block;margin:3rem auto 1.5rem}.home .hero h1{font-size:3rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.8rem auto}.home .hero .actions{display:flex;flex-wrap:wrap;gap:1rem;justify-content:center}.home .hero .description{max-width:35rem;font-size:1.6rem;line-height:1.3;color:var(--c-text-lightest)}.home .hero .action-button{display:inline-block;font-size:1.2rem;padding:.8rem 1.6rem;border-width:2px;border-style:solid;border-radius:4px;transition:background-color var(--t-color);box-sizing:border-box}.home .hero .action-button.primary{color:var(--c-bg);background-color:var(--c-brand);border-color:var(--c-brand)}.home .hero .action-button.primary:hover{background-color:var(--c-brand-light)}.home .hero .action-button.secondary{color:var(--c-brand);background-color:var(--c-bg);border-color:var(--c-brand)}.home .hero .action-button.secondary:hover{color:var(--c-bg);background-color:var(--c-brand-light)}.home .features{border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding:1.2rem 0;margin-top:2.5rem;display:flex;flex-wrap:wrap;align-items:flex-start;align-content:stretch;justify-content:space-between}.home .feature{flex-grow:1;flex-basis:30%;max-width:30%}.home .feature h2{font-size:1.4rem;font-weight:500;border-bottom:none;padding-bottom:0;color:var(--c-text-light)}.home .feature p{color:var(--c-text-lighter)}.home .theme-default-content{padding:0;margin:0}.home .footer{padding:2.5rem;border-top:1px solid var(--c-border);text-align:center;color:var(--c-text-lighter);transition:border-color var(--t-color)}@media (max-width: 719px){.home .features{flex-direction:column}.home .feature{max-width:100%;padding:0 2.5rem}}@media (max-width: 419px){.home{padding-left:1.5rem;padding-right:1.5rem}.home .hero img{max-height:210px;margin:2rem auto 1.2rem}.home .hero h1{font-size:2rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.2rem auto}.home .hero .description{font-size:1.2rem}.home .hero .action-button{font-size:1rem;padding:.6rem 1.2rem}.home .feature h2{font-size:1.25rem}}.page{padding-top:var(--navbar-height);padding-left:var(--sidebar-width)}.navbar{position:fixed;z-index:20;top:0;left:0;right:0;height:var(--navbar-height);box-sizing:border-box;border-bottom:1px solid var(--c-border);background-color:var(--c-bg-navbar);transition:background-color var(--t-color),border-color var(--t-color)}.sidebar{font-size:16px;width:var(--sidebar-width);position:fixed;z-index:10;margin:0;top:var(--navbar-height);left:0;bottom:0;box-sizing:border-box;border-right:1px solid var(--c-border);overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--c-brand) var(--c-border);background-color:var(--c-bg-sidebar);transition:transform var(--t-transform),background-color var(--t-color),border-color var(--t-color)}.sidebar::-webkit-scrollbar{width:7px}.sidebar::-webkit-scrollbar-track{background-color:var(--c-border)}.sidebar::-webkit-scrollbar-thumb{background-color:var(--c-brand)}.sidebar-mask{position:fixed;z-index:9;top:0;left:0;width:100vw;height:100vh;display:none}.theme-container.sidebar-open .sidebar-mask{display:block}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1){transform:rotate(45deg) translate3d(5.5px,5.5px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(2){transform:scale3d(0,1,1)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform:rotate(-45deg) translate3d(6px,-6px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1),.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform-origin:center}.theme-container.no-navbar .theme-default-content h1,.theme-container.no-navbar .theme-default-content h2,.theme-container.no-navbar .theme-default-content h3,.theme-container.no-navbar .theme-default-content h4,.theme-container.no-navbar .theme-default-content h5,.theme-container.no-navbar .theme-default-content h6{margin-top:1.5rem;padding-top:0}.theme-container.no-navbar .page{padding-top:0}.theme-container.no-navbar .sidebar{top:0}.theme-container.no-sidebar .sidebar{display:none}@media (max-width: 719px){.theme-container.no-sidebar .sidebar{display:block}}.theme-container.no-sidebar .page{padding-left:0}.theme-default-content a:not(.header-anchor):hover{text-decoration:underline}.theme-default-content img{max-width:100%}.theme-default-content h1,.theme-default-content h2,.theme-default-content h3,.theme-default-content h4,.theme-default-content h5,.theme-default-content h6{margin-top:calc(.5rem - var(--navbar-height));padding-top:calc(1rem + var(--navbar-height));margin-bottom:0}.theme-default-content h1:first-child,.theme-default-content h2:first-child,.theme-default-content h3:first-child,.theme-default-content h4:first-child,.theme-default-content h5:first-child,.theme-default-content h6:first-child{margin-bottom:1rem}.theme-default-content h1:first-child+p,.theme-default-content h1:first-child+pre,.theme-default-content h1:first-child+.custom-container,.theme-default-content h2:first-child+p,.theme-default-content h2:first-child+pre,.theme-default-content h2:first-child+.custom-container,.theme-default-content h3:first-child+p,.theme-default-content h3:first-child+pre,.theme-default-content h3:first-child+.custom-container,.theme-default-content h4:first-child+p,.theme-default-content h4:first-child+pre,.theme-default-content h4:first-child+.custom-container,.theme-default-content h5:first-child+p,.theme-default-content h5:first-child+pre,.theme-default-content h5:first-child+.custom-container,.theme-default-content h6:first-child+p,.theme-default-content h6:first-child+pre,.theme-default-content h6:first-child+.custom-container{margin-top:2rem}@media (max-width: 959px){.sidebar{font-size:15px;width:var(--sidebar-width-mobile)}.page{padding-left:var(--sidebar-width-mobile)}}@media (max-width: 719px){.sidebar{top:0;padding-top:var(--navbar-height);transform:translate(-100%)}.page{padding-left:0}.theme-container.sidebar-open .sidebar{transform:translate(0)}.theme-container.no-navbar .sidebar{padding-top:0}}@media (max-width: 419px){h1{font-size:1.9rem}}#vp-comment{max-width:var(--content-width);margin:0 auto;padding:2rem 2.5rem}@media (max-width: 959px){#vp-comment{padding:2rem}}@media (max-width: 419px){#vp-comment{padding:1.5rem}}.navbar{--navbar-line-height: calc( var(--navbar-height) - 2 * var(--navbar-padding-v) );padding:var(--navbar-padding-v) var(--navbar-padding-h);line-height:var(--navbar-line-height)}.navbar .logo{height:var(--navbar-line-height);margin-right:var(--navbar-padding-v);vertical-align:top}.navbar .site-name{font-size:1.3rem;font-weight:600;color:var(--c-text);position:relative}.navbar .navbar-items-wrapper{display:flex;position:absolute;box-sizing:border-box;top:var(--navbar-padding-v);right:var(--navbar-padding-h);height:var(--navbar-line-height);padding-left:var(--navbar-padding-h);white-space:nowrap;font-size:.9rem}.navbar .navbar-items-wrapper .search-box{flex:0 0 auto;vertical-align:top}@media screen and (max-width: 719px){.navbar{padding-left:4rem}.navbar .site-name{display:block;width:calc(100vw - 11rem);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.navbar .can-hide{display:none}}.navbar-items{display:inline-block}@media print{.navbar-items{display:none}}.navbar-items a{display:inline-block;line-height:1.4rem;color:inherit}.navbar-items a:hover,.navbar-items a.route-link-active{color:var(--c-text)}.navbar-items .navbar-item{position:relative;display:inline-block;margin-left:1.5rem;line-height:var(--navbar-line-height)}.navbar-items .navbar-item:first-child{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.route-link-active{margin-bottom:-2px;border-bottom:2px solid var(--c-text-accent)}@media (max-width: 719px){.navbar-items .navbar-item{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.route-link-active{margin-bottom:0;border-bottom:none}.navbar-items a:hover,.navbar-items a.route-link-active{color:var(--c-text-accent)}}.toggle-sidebar-button{position:absolute;top:.6rem;left:1rem;display:none;padding:.6rem;cursor:pointer}.toggle-sidebar-button .icon{display:flex;flex-direction:column;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;cursor:inherit}.toggle-sidebar-button .icon span{display:inline-block;width:100%;height:2px;border-radius:2px;background-color:var(--c-text);transition:transform var(--t-transform)}.toggle-sidebar-button .icon span:nth-child(2){margin:6px 0}@media screen and (max-width: 719px){.toggle-sidebar-button{display:block}}.toggle-color-mode-button{display:flex;margin:auto;margin-left:1rem;border:0;background:none;color:var(--c-text);opacity:.8;cursor:pointer}@media print{.toggle-color-mode-button{display:none}}.toggle-color-mode-button:hover{opacity:1}.toggle-color-mode-button .icon{width:1.25rem;height:1.25rem}.DocSearch{transition:background-color var(--t-color)}.navbar-dropdown-wrapper{cursor:pointer}.navbar-dropdown-wrapper .navbar-dropdown-title,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:block;font-size:.9rem;font-family:inherit;cursor:inherit;padding:inherit;line-height:1.4rem;background:transparent;border:none;font-weight:500;color:var(--c-text)}.navbar-dropdown-wrapper .navbar-dropdown-title:hover,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{border-color:transparent}.navbar-dropdown-wrapper .navbar-dropdown-title .arrow,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile .arrow{vertical-align:middle;margin-top:-1px;margin-left:.4rem}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:none;font-weight:600;font-size:inherit}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item{color:inherit;line-height:1.7rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{margin:.45rem 0 0;border-top:1px solid var(--c-border);padding:1rem 0 .45rem;font-size:.9rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>span{padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a{font-weight:inherit}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a.route-link-active:after{display:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper{padding:0;list-style:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper .navbar-dropdown-subitem{font-size:.9em}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a{display:block;line-height:1.7rem;position:relative;border-bottom:none;font-weight:400;margin-bottom:0;padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a:hover,.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.route-link-active{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.route-link-active:after{content:"";width:0;height:0;border-left:5px solid var(--c-text-accent);border-top:3px solid transparent;border-bottom:3px solid transparent;position:absolute;top:calc(50% - 2px);left:9px}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item:first-child .navbar-dropdown-subtitle{margin-top:0;padding-top:0;border-top:0}.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title-mobile{margin-bottom:.5rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:none}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:block}.navbar-dropdown-wrapper.mobile .navbar-dropdown{transition:height .1s ease-out;overflow:hidden}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{border-top:0;margin-top:0;padding-top:0;padding-bottom:0}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle,.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item>a{font-size:15px;line-height:2rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem{font-size:14px;padding-left:1rem}.navbar-dropdown-wrapper:not(.mobile){height:1.8rem}.navbar-dropdown-wrapper:not(.mobile):hover .navbar-dropdown,.navbar-dropdown-wrapper:not(.mobile).open .navbar-dropdown{display:block!important}.navbar-dropdown-wrapper:not(.mobile).open:blur{display:none}.navbar-dropdown-wrapper:not(.mobile) .navbar-dropdown{display:none;height:auto!important;box-sizing:border-box;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;background-color:var(--c-bg-navbar);padding:.6rem 0;border:1px solid var(--c-border);border-bottom-color:var(--c-border-dark);text-align:left;border-radius:.25rem;white-space:nowrap;margin:0}.page{padding-bottom:2rem;display:block}.page .theme-default-content{max-width:var(--content-width);margin:0 auto;padding:2rem 2.5rem;padding-top:0}@media (max-width: 959px){.page .theme-default-content{padding:2rem}}@media (max-width: 419px){.page .theme-default-content{padding:1.5rem}}.sidebar ul{padding:0;margin:0;list-style-type:none}.sidebar a{display:inline-block}.sidebar .navbar-items{display:none;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color);padding:.5rem 0 .75rem}.sidebar .navbar-items a{font-weight:600}.sidebar .navbar-items .navbar-item{display:block;line-height:1.25rem;font-size:1.1em;padding:.5rem 0 .5rem 1.5rem}.sidebar .sidebar-items{padding:1.5rem 0}@media (max-width: 719px){.sidebar .navbar-items{display:block}.sidebar .navbar-items .navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.route-link-active:after{top:calc(1rem - 2px)}.sidebar .sidebar-items{padding:1rem 0}}.sidebar-item{cursor:default;border-left:.25rem solid transparent;color:var(--c-text)}.sidebar-item:focus-visible{outline-width:1px;outline-offset:-1px}.sidebar-item.active:not(p.sidebar-heading){font-weight:600;color:var(--c-text-accent);border-left-color:var(--c-text-accent)}.sidebar-item.sidebar-heading{transition:color .15s ease;font-size:1.1em;font-weight:700;padding:.35rem 1.5rem .35rem 1.25rem;width:100%;box-sizing:border-box;margin:0}.sidebar-item.sidebar-heading+.sidebar-item-children{transition:height .1s ease-out;overflow:hidden;margin-bottom:.75rem}.sidebar-item.collapsible{cursor:pointer}.sidebar-item.collapsible .arrow{position:relative;top:-.12em;left:.5em}.sidebar-item:not(.sidebar-heading){font-size:1em;font-weight:400;display:inline-block;margin:0;padding:.35rem 1rem .35rem 2rem;line-height:1.4;width:100%;box-sizing:border-box}.sidebar-item:not(.sidebar-heading)+.sidebar-item-children{padding-left:1rem;font-size:.95em}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading){padding:.25rem 1rem .25rem 1.75rem}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading).active{font-weight:500;border-left-color:transparent}a.sidebar-heading+.sidebar-item-children .sidebar-item:not(.sidebar-heading).active{border-left-color:transparent}a.sidebar-item{cursor:pointer}a.sidebar-item:hover{color:var(--c-text-accent)}.table-of-contents .badge{vertical-align:middle}.dropdown-enter-from,.dropdown-leave-to{height:0!important}.fade-slide-y-enter-active{transition:all .2s ease}.fade-slide-y-leave-active{transition:all .2s cubic-bezier(1,.5,.8,1)}.fade-slide-y-enter-from,.fade-slide-y-leave-to{transform:translateY(10px);opacity:0}:root{--c-brand: #2176d6;--c-brand-light: #00c3ff}html.dark{--c-brand: #2176d6;--c-brand-light: #00c3ff}:root{--search-bg-color: #ffffff;--search-accent-color: #3eaf7c;--search-text-color: #2c3e50;--search-border-color: #eaecef;--search-item-text-color: #5d81a5;--search-item-focus-bg-color: #f3f4f5;--search-input-width: 8rem;--search-result-width: 20rem}.search-box{display:inline-block;position:relative;margin-left:1rem}@media print{.search-box{display:none}}.search-box input{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:text;width:var(--search-input-width);height:2rem;color:var(--search-text-color);display:inline-block;border:1px solid var(--search-border-color);border-radius:2rem;font-size:.9rem;line-height:2rem;padding:0 .5rem 0 2rem;outline:none;transition:all ease .3s;background:var(--search-bg-color) url("data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='UTF-8'?%3e%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='12'%20height='13'%3e%3cg%20stroke-width='2'%20stroke='%23aaa'%20fill='none'%3e%3cpath%20d='M11.29%2011.71l-4-4'/%3e%3ccircle%20cx='5'%20cy='5'%20r='4'/%3e%3c/g%3e%3c/svg%3e") .6rem .5rem no-repeat;background-size:1rem}@media (max-width: 719px){.search-box input{cursor:pointer;width:0;border-color:transparent;position:relative}}.search-box input:focus{cursor:auto;border-color:var(--search-accent-color)}@media (max-width: 719px){.search-box input:focus{cursor:text;left:0;width:10rem}}@media (max-width: 419px){.search-box input:focus{width:8rem}}.search-box .suggestions{background:var(--search-bg-color);width:var(--search-result-width);position:absolute;top:2rem;right:0;border:1px solid var(--search-border-color);border-radius:6px;padding:.4rem;list-style-type:none}@media (max-width: 419px){.search-box .suggestions{width:calc(100vw - 4rem);right:-.5rem}}.search-box .suggestion{line-height:1.4;padding:.4rem .6rem;border-radius:4px;cursor:pointer}.search-box .suggestion a{white-space:normal;color:var(--search-item-text-color)}.search-box .suggestion.focus{background-color:var(--search-item-focus-bg-color)}.search-box .suggestion.focus a{color:var(--search-accent-color)}.search-box .suggestion .page-title{font-weight:600}.search-box .suggestion .page-header{font-size:.9em;margin-left:.25em}@keyframes rotate{0%{transform:rotate(0)}50%{transform:rotate(360deg)}to{transform:rotate(360deg)}}.popup-enter-active,.popup-leave-active{transition:opacity .3s,transform .3s}.popup-enter-from,.popup-leave-to{opacity:0;transform:translateY(50%) scale(.5)}.sw-hint-popup,.sw-update-popup{position:fixed;bottom:1rem;inset-inline-end:1rem;z-index:var(--pwa-z-index);padding:.5rem .75rem;border-width:0;border-radius:.5rem;background:var(--pwa-bg-color);color:var(--pwa-color);box-shadow:0 2px 12px 0 var(--pwa-shadow-color);font-size:1rem;line-height:1.5;cursor:pointer}@media print{.sw-hint-popup,.sw-update-popup{display:none}}.sw-hint-popup .icon-wrapper,.sw-update-popup .icon-wrapper{display:inline-block;vertical-align:middle;width:1.5rem;height:1.5rem;margin-inline-start:.4rem;border-radius:.75rem;background:var(--pwa-btn-bg-color)}.sw-hint-popup .icon-wrapper:hover,.sw-update-popup .icon-wrapper:hover{background:var(--pwa-btn-hover-bg-color)}.sw-hint-popup .icon-wrapper svg,.sw-update-popup .icon-wrapper svg{width:1.2rem;height:1.2rem;margin:.15rem;color:var(--pwa-btn-text-color);animation:rotate 3s ease infinite}:root{--pwa-z-index: 10;--pwa-color: #2c3e50;--pwa-bg-color: #ffffff;--pwa-border-color: #3eaf7c;--pwa-shadow-color: rgb(0 0 0 / 15%);--pwa-btn-text-color: #ffffff;--pwa-btn-bg-color: #3eaf7c;--pwa-btn-hover-bg-color: #4abf8a;--pwa-content-color: #333;--pwa-content-light-color: #666}:root{--medium-zoom-z-index: 100;--medium-zoom-bg-color: #ffffff;--medium-zoom-opacity: 1}.medium-zoom-overlay{background-color:var(--medium-zoom-bg-color)!important;z-index:var(--medium-zoom-z-index)}.medium-zoom-overlay~img{z-index:calc(var(--medium-zoom-z-index) + 1)}.medium-zoom--opened .medium-zoom-overlay{opacity:var(--medium-zoom-opacity)}html.dark{--box-shadow: #0f0e0d;--card-shadow: rgba(0, 0, 0, .3);--black: #fff;--grey-dark: #999;--grey-light: #666;--white: #000;--grey-darker: #bbb;--grey-lighter: #333;--grey14: #111}:root{--vp-bg: var(--c-bg, #fff);--vp-bgl: var(--c-bg-light, #f3f4f5);--vp-bglt: var(--c-bg-lighter, #eeeeee);--vp-c: var(--c-text, #2c3e50);--vp-cl: var(--c-text-light, #3a5169);--vp-clt: var(--c-text-lighter, #4e6e8e);--vp-brc: var(--c-border, #eaecef);--vp-brcd: var(--c-border-dark, #dfe2e5);--vp-tc: var(--c-brand, #3eaf7c);--vp-tcl: var(--c-brand-light, #4abf8a);--vp-ct: var(--t-color, .3s ease);--vp-tt: var(--t-transform, .3s ease);--box-shadow: #f0f1f2;--card-shadow: rgba(0, 0, 0, .15);--black: #000;--grey-dark: #666;--grey-light: #999;--white: #fff;--grey-darker: #333;--grey-lighter: #bbb;--grey14: #eee}.theme-default-content figure{position:relative;display:flex;flex-direction:column;width:auto;margin:1rem auto;text-align:center;transition:transform var(--vp-tt)}.theme-default-content figure img{overflow:hidden;margin:0 auto;border-radius:8px}.theme-default-content figure img[tabindex]:hover,.theme-default-content figure img[tabindex]:focus{box-shadow:2px 2px 10px 0 var(--card-shadow)}@media print{.theme-default-content figure>a[href^="http://"]:after,.theme-default-content figure>a[href^="https://"]:after{content:""}}.theme-default-content figure>a .external-link-icon{display:none}.theme-default-content figure figcaption{display:inline-block;margin:6px auto;font-size:.8rem}html:not(.dark) figure:has(img[data-mode=darkmode-only]),html:not(.dark) img[data-mode=darkmode-only]{display:none!important}html.dark figure:has(img[data-mode=lightmode-only]),html.dark img[data-mode=lightmode-only]{display:none!important} diff --git a/assets/swftui-playground-MsNlfoqb.png b/assets/swftui-playground-MsNlfoqb.png new file mode 100644 index 0000000..e49f618 Binary files /dev/null and b/assets/swftui-playground-MsNlfoqb.png differ diff --git a/assets/swiftui-framework-wwdc-OMsLBsB_.jpg b/assets/swiftui-framework-wwdc-OMsLBsB_.jpg new file mode 100644 index 0000000..d673c12 Binary files /dev/null and b/assets/swiftui-framework-wwdc-OMsLBsB_.jpg differ diff --git a/assets/xcode-new-project-BgcBHlMK.png b/assets/xcode-new-project-BgcBHlMK.png new file mode 100644 index 0000000..5e6a509 Binary files /dev/null and b/assets/xcode-new-project-BgcBHlMK.png differ diff --git a/assets/xcodes-DaBHvxPG.png b/assets/xcodes-DaBHvxPG.png new file mode 100644 index 0000000..c168fea Binary files /dev/null and b/assets/xcodes-DaBHvxPG.png differ diff --git a/browserconfig.xml b/browserconfig.xml new file mode 100644 index 0000000..72dcbee --- /dev/null +++ b/browserconfig.xml @@ -0,0 +1,11 @@ + +// swift-tools-version: 5.6 + +// WARNING: +// This file is automatically generated. +// Do not edit it by hand because the contents will be replaced. + +import PackageDescription +import AppleProductTypes + +let package = Package( + name: "Moovy", + platforms: [ + .iOS("15.2"), + .macOS("13.0") + ], + products: [ + .iOSApplication( + name: "Moovy", + targets: ["AppModule"], + displayVersion: "1.0", + bundleVersion: "1", + appIcon: .placeholder(icon: .sun), + accentColor: .presetColor(.indigo), + supportedDeviceFamilies: [ + .pad, + .phone + ], + supportedInterfaceOrientations: [ + .portrait, + .landscapeRight, + .landscapeLeft, + .portraitUpsideDown(.when(deviceFamilies: [.pad])) + ], + capabilities: [ + .outgoingNetworkConnections() + ], + appCategory: .entertainment + ) + ], + dependencies: [ + .package(url: "https://github.com/Tunous/DebouncedOnChange.git", "1.0.0"..<"2.0.0"), + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", "4.0.0"..<"5.0.0") + ], + targets: [ + .executableTarget( + name: "AppModule", + dependencies: [ + "DebouncedOnChange", + "KeychainAccess" + ], + path: "." + ) + ] +) +
+ \ No newline at end of file diff --git a/favicon-16x16.png b/favicon-16x16.png new file mode 100644 index 0000000..1e94c2e Binary files /dev/null and b/favicon-16x16.png differ diff --git a/favicon-32x32.png b/favicon-32x32.png new file mode 100644 index 0000000..8fdc5f7 Binary files /dev/null and b/favicon-32x32.png differ diff --git a/favicon-96x96.png b/favicon-96x96.png new file mode 100644 index 0000000..66c2062 Binary files /dev/null and b/favicon-96x96.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..e2bcc48 Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..2291df1 --- /dev/null +++ b/index.html @@ -0,0 +1,37 @@ + + + + + + + + ++ ++ ++ + + #ffffff +Welcome | iOS Training + + + + + ++ + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..2822660 Binary files /dev/null and b/logo.png differ diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..c9478c0 --- /dev/null +++ b/manifest.webmanifest @@ -0,0 +1 @@ +{"name":"Worldline iOS training","short_name":"iOS Training","description":"iOS developer beginner training","lang":"en-US","start_url":"index.html","scope":"/ios-training/","display":"standalone","theme_color":"#46bd87","background_color":"#ffffff","orientation":"portrait-primary","prefer_related_applications":false,"icons":[{"src":"android-icon-36x36.png","sizes":"36x36","type":"image/png","density":"0.75"},{"src":"android-icon-48x48.png","sizes":"48x48","type":"image/png","density":"1.0"},{"src":"android-icon-72x72.png","sizes":"72x72","type":"image/png","density":"1.5"},{"src":"android-icon-96x96.png","sizes":"96x96","type":"image/png","density":"2.0"},{"src":"android-icon-144x144.png","sizes":"144x144","type":"image/png","density":"3.0"},{"src":"android-icon-192x192.png","sizes":"192x192","type":"image/png","density":"4.0"}]} diff --git a/mini-project/index.html b/mini-project/index.html new file mode 100644 index 0000000..7f3e94e --- /dev/null +++ b/mini-project/index.html @@ -0,0 +1,120 @@ + + + + + + + + +Mini project | iOS Training + + + + + ++ + + diff --git a/ms-icon-144x144.png b/ms-icon-144x144.png new file mode 100644 index 0000000..608eafc Binary files /dev/null and b/ms-icon-144x144.png differ diff --git a/ms-icon-150x150.png b/ms-icon-150x150.png new file mode 100644 index 0000000..9485f03 Binary files /dev/null and b/ms-icon-150x150.png differ diff --git a/ms-icon-310x310.png b/ms-icon-310x310.png new file mode 100644 index 0000000..a686259 Binary files /dev/null and b/ms-icon-310x310.png differ diff --git a/ms-icon-70x70.png b/ms-icon-70x70.png new file mode 100644 index 0000000..1109ef7 Binary files /dev/null and b/ms-icon-70x70.png differ diff --git a/persist-data/index.html b/persist-data/index.html new file mode 100644 index 0000000..6ed80e4 --- /dev/null +++ b/persist-data/index.html @@ -0,0 +1,39 @@ + + + + + + + + +Mini project
The final chapter of this training will ask you to create a SwiftUI app from scratch.
Requirements
The app consists of a movie explorer app with the following features:
- Search for movies by title.
- View the details of the selected movie.
- The app requires the user to be logged in.
- The app allows a new user to register.
- The movie list screen allows to logout from the app.
- The app remembers the logged in user after a restart.
- The app uses this API for the authenticating and searching for movies.
- The /movies/search endpoint requires to pass the token retrieved from endpoint /user/login or user/register in this header:
Authorization: Bearer \(userResponse.token)
- (Optional) The result of previous queries is locally cached.
- (Optional) Add movie to local favorites ⭐️
- (Optional) Animate the transition between the login view and the movie list view (tutorial).
A preview of the app can be seen here.
Hints
- There are many techniques to handle the flow from the login view to the movie list view. On of them is to rely on a logged state. The following gives an overview how it looks like.
struct ContentView: View { + @State var loggedIn: false + + var body: some View { + if loggedIn { + MovieListView() + } else { + // The LoginView takes a callback that is called when the login succeeds + LoginView { newLoggedIn in + loggedIn = newLoggedIn + } + } + } +} +
- In the login view, use an enum to track the state of the login operation so that you can disable the login button when a request is running.
enum LoginState { + case neutral, loading, success, failure +} +struct LoginView: View { + @State private var loginState: LoginState = .neutral + // other code +} +
- Use a Task object to run async code.
Button("Login") { + loginState = .loading + Task { + if await login() { + onLoginSuccess(true) + } + } +} +
Swift Concurrency crashes on Swift Playground
Do not use the Swift Playground app to run you app as it does not work well with SwiftUI + Swift Concurrency (async, await and Task). Instead, you can create an Xcode project of type Playground to combine the power of Xcode and the simplicity of Playground projects.
- Use DebouncedOnChange Swift package to optimize search.
- To generate the initial code for a preview, open a view and then use the Xcode feature Editor -> Create preview
- The List view requires that you specify an
id
fieldList(movies, id: \.title)
or that the items conform to Identifiable protocol- If you can't add SwiftPM packages from Xcode, add them by editing the package.swift file by hand. Here is an example below.
// swift-tools-version: 5.6 + +// WARNING: +// This file is automatically generated. +// Do not edit it by hand because the contents will be replaced. + +import PackageDescription +import AppleProductTypes + +let package = Package( + name: "Moovy", + platforms: [ + .iOS("15.2"), + .macOS("13.0") + ], + products: [ + .iOSApplication( + name: "Moovy", + targets: ["AppModule"], + displayVersion: "1.0", + bundleVersion: "1", + appIcon: .placeholder(icon: .sun), + accentColor: .presetColor(.indigo), + supportedDeviceFamilies: [ + .pad, + .phone + ], + supportedInterfaceOrientations: [ + .portrait, + .landscapeRight, + .landscapeLeft, + .portraitUpsideDown(.when(deviceFamilies: [.pad])) + ], + capabilities: [ + .outgoingNetworkConnections() + ], + appCategory: .entertainment + ) + ], + dependencies: [ + .package(url: "https://github.com/Tunous/DebouncedOnChange.git", "1.0.0"..<"2.0.0"), + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", "4.0.0"..<"5.0.0") + ], + targets: [ + .executableTarget( + name: "AppModule", + dependencies: [ + "DebouncedOnChange", + "KeychainAccess" + ], + path: "." + ) + ] +) +
Locally persisting data | iOS Training + + + + + ++ + + diff --git a/presentation/index.html b/presentation/index.html new file mode 100644 index 0000000..e605226 --- /dev/null +++ b/presentation/index.html @@ -0,0 +1,37 @@ + + + + + + + + +Locally persisting data
Estimated time
1/4 day
Persisting data locally consists of keeping app data after the app has been killed and the variables removed from memory. The persisted data offers many advantages. We can use it to show initial data when the app starts and waits for the first batch data to be fetched from the server. It can also be used to allow for offline app usage.
iOS isolates app data from other apps
For security reasons, each app is isolated from the rest of the apps. This is called sandboxing. This article shows the different ways that allow two or more apps to share their data
There are many ways to persist data in SwiftUI that we cover below.
UserDefaults
It is a very simple key-value storage that persists data in a file. The API surface is very small and the developer does not need to manage the persisted file. This makes this technique very efficient for simple storage use cases. You can find a short guide here.
Here is sample code that shows how to persist and load data.
UserDefaults.standard.set(self.tapCount, forKey: "Tap") +@State private var tapCount = UserDefaults.standard.integer(forKey: "Tap") +
Codable
saved in a fileA more advanced and powerful technique is to manually load and persist a
Codable
into a file. This technique is useful if you want to store complex objects (such as the state or model) in a JSON file. There are two steps in this process, the first one consists of decoding / encoding the object from / into JSON usingJSONDecoder().decode
andJSONEncoder().encode
. The second step consists of loading / saving the encoded data and we can think of two ways to achieve this. The first one consists of user defaults'dataForKey:
to load the data andsetObject:ForKey
to persist it. Another one consists of creating and managing a file by the developer using file APIs such asfileHandle.availableData
to load the data from a file anddata.write
to save it.Sophisticated data persistence libraries
For storing data in a database or similar fashion, SQLite is available as a low level library. It is not recommended to use it unless there is a strong performance concern. Instead, it is recommended to use libraries specialized in data persistence. Some can be assimilated to an ORM library (Object Relational Mapper). The remainder of this section describes some of them.
Please be careful about the pricing of cloud storage
Sophisticated databases generally provide cloud storage to provide a complete offer. If you're interested in storing data in the cloud, please take some time to read the pricing page to avoid any bad surprises when your app runs in production.
Core Data
Core Data is the official library to "Persist or cache data on a single device, or sync data to multiple devices with CloudKit". It existed since iOS 3 and Apple continuously updates it to keep it relevant. It also has the reputation of having a steep learning curve, but it remains famous among developers.
It works similarly as an ORM where classes are mapped into tables. Xcode provides a graphical editor that allows to specify the tables, the relations and generate the necessary code (in Swift or Objective-C).
Even though Core Date existed before SwiftUI, Apple made sure that both of them can be used together. This article shows how to use Core Data in a SwiftUI project.
Realm
Realm is a high level alternative to SQLite. It can be seen as alternative to Core Data as they seem to provide a similar list of features. Most notably, the possibility to store data locally or in the cloud. The points where Realm wins is that the library seems simpler to learn and to use and that it is also available in Android.
Firebase datastore (or any other cloud based storage)
As opposed to Realm and Core Data, which are local first databases, Firebase datastore is a cloud first database. This means that Firebase Datastore requires an internet connection to store and load the data. However, the library is simple to use and supports real time updates.
TIP
Firebase datastore is part of a bigger suite of service called Firebase. For example, we can Firebase App Distribution in Firebase, which is a service that allows to deploy and distribute apps without going the burden of using TestFlight.
PW: complete the official iOS persisting data tutorial
This PW shows how to save a
Codable
in a manually managed file using JSON encoder and filesystem APIs.https://developer.apple.com/tutorials/app-dev-training/persisting-data
Presentation | iOS Training + + + + + ++ + + diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 0000000..1dc4742 --- /dev/null +++ b/service-worker.js @@ -0,0 +1 @@ +if(!self.define){let e,s={};const i=(i,t)=>(i=new URL(i+".js",t).href,s[i]||new Promise((s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()})).then((()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e})));self.define=(t,r)=>{const d=e||("document"in self?document.currentScript.src:"")||location.href;if(s[d])return;let n={};const l=e=>i(e,d),c={module:{uri:d},exports:n,require:l};s[d]=Promise.all(t.map((e=>c[e]||l(e)))).then((e=>(r(...e),n)))}}define(["./workbox-1ab968a5"],(function(e){"use strict";self.addEventListener("message",(e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()})),e.clientsClaim(),e.precacheAndRoute([{url:"assets/404.html-9pEcDQrh.js",revision:"ec8d0613063a8efe33850b20ea9a86d7"},{url:"assets/app-Bbun9eEO.js",revision:"2065692954e7cf20d865050915113c60"},{url:"assets/index-DTEEl-sV.js",revision:"46a193641571106d3b7b43f9bc2a2735"},{url:"assets/index.html-5n7MuywO.js",revision:"009154f00c5932eb724eb96396250ae4"},{url:"assets/index.html-B8dggsdP.js",revision:"198774e301bf751241b4bb9b9f9153bd"},{url:"assets/index.html-C39pB5w2.js",revision:"89a0fdb527a8b6f74b08e2d90c4d9f7e"},{url:"assets/index.html-CfxHtkWF.js",revision:"583b5fe12bc77bb25db79e451d5d9a24"},{url:"assets/index.html-ChAAQVq_.js",revision:"d91b5994055d7f83023b05963bde6214"},{url:"assets/index.html-Cv2fRjYf.js",revision:"5a1ac78a3cb7017fe5cce5468a70b83d"},{url:"assets/index.html-DKuyFSlS.js",revision:"60b6fbeb32a391781b5cf778b54c1b11"},{url:"assets/index.html-DOlN3MeL.js",revision:"99fd78a922e4c8dc345cb6df332af5f7"},{url:"assets/index.html-z4YYCP43.js",revision:"1ff4fd171aed54781d715261e048ef51"},{url:"assets/style-DGf4msjw.css",revision:"8230bc058d85c85b89623d99eba76ed6"},{url:"index.html",revision:"4f4c745857f8b16c96faccbda6b001f5"},{url:"404.html",revision:"e90035ac2e19c4100209c50a1c28c1af"}],{}),e.cleanupOutdatedCaches()})); diff --git a/styles.scss b/styles.scss new file mode 100644 index 0000000..155d241 --- /dev/null +++ b/styles.scss @@ -0,0 +1,191 @@ +/*! + * Forked from Writ v1.0.4 + * Copyright © 2015, Curtis McEnroePresentation
Welcompe the world of iOS development
iOS development consists of developing applications that can target mainly the iPhone and iPad but also macOS, iWatch and Apple TV. There are many ways to achieve this:
- Using the official frameworks and tools provided by Apple
- Using 3rd party frameworks and tools such as Capacitor, MAUI and Flutter
This training focuses on iOS development using the official tools and frameworks proposed by Apple. Our development stack will consist of the following items:
- Programming language: Swift
- UI Framework: SwiftUI
- IDEs: Xcode and Swift Playgrounds
In addition to that, It is also possible to leverage the Swift language (without SwiftUI) in order to develop console apps and servers on Window, Linux, macOS.
History
The early days of iOS development used the Objective-C language, the UIKit UI Framework and -the good old- Xcode. This ecosystem was basic but quite powerful and allowed to develop amazing apps. The continuous updates from Apple improved the developer experience. For example, memory management became automatic (thanks to ARC) and the layout system became capable of adapting to different screen sizes.
In WWDC 2014, Apple announced the Swift language as an Open Source modern replacement to Objective-C. Following that, apple announced during the next WWDC SwiftUI as the replacement for UIKit.
As of 2021, the majority of new iOS projects use Swift and SwiftUI with UIKit as a fallback for the UI aspects.
Getting started
- On macOS, install Xcodes and Swift Playgrounds.
- Xcodes is a tools that downloads and manages the different versions of Xcode.
- For windows and Linux users you can install Swift for Windows or Swift for Ubuntu. This will allow you to run Swift code on your machine.
- You can use VSCode or Fleet as an IDE.
- Unfortunately, SwiftUI development either challenging or not supported. (on Windows you can try to use swift-win32)
- Open a terminal and run the following command to check if Swift is installed
swift --version
.Create a CLI app with swift CLI
- Open a terminal in an empty folder
mkdir MyCLI
and thencd MyCLI
- Create the project with
swift package init --name MyCLI --type executable
- and run it with
swift run
. It should print -> Hello worldCreate an App using Xcode or Swift playgrounds
- On Xcode, create a either a Project or Playground and run it.
Swift project managers
- Official tools:
- Swift Package Manager (also called swiftpm) is the official tool to manage Swift projects. It is platform and IDE agnostic.
- Xcode projects are projects fully managed by Xcode (.xcodeproj)
- 3rd party tools:
In this training, we'll use swiftpm and Xcode.
Links and references
+ * https://cmcenroe.me/writ/LICENSE (ISC) + */ + +/* Fonts, sizes & vertical rhythm */ + +html { + font-family: Palatino, Georgia, Lucida Bright, Book Antiqua, serif; + font-size: 16px; + line-height: 1.5rem; +} + +h1, h2, h3, h4, h5, h6, th { font-weight: normal; } + +/* Minor third */ +h1 { font-size: 2.488em; } +h2 { font-size: 2.074em; } +h3 { font-size: 1.728em; } +h4 { font-size: 1.44em; } +h5 { font-size: 1.2em; } +h6 { font-size: 1em; } +small { font-size: 0.833em; } + +h1, h2, h3 { line-height: 3rem; } + +p, ul, ol, dl, table, blockquote, pre, h1, h2, h3, h4, h5, h6 { + margin: 1.5rem 0 0; +} +ul ul, ol ol, ul ol, ol ul { margin: 0; } + +hr { + margin: 0; + border: none; + padding: 1.5rem 0 0; +} + +/* Colors */ + +body { color: #222; background-color: #fcfcfc; } +code, pre, samp, kbd { color: #111; } +a, header nav a:visited, a code { color: #00e; } +a:visited, a:visited code { color: #60b; } +mark { color: inherit; } + +code, pre, samp, thead, tfoot { background-color: rgba(0, 0, 0, 0.05); } +mark { background-color: #fe0; } + +main aside, blockquote, ins { border: solid rgba(0, 0, 0, 0.05); } +pre, code, samp { border: solid rgba(0, 0, 0, 0.1); } +th, td { border: solid #dbdbdb; } + +/* Layout */ + +body { margin: 1.5rem 1ch; } + +body > header { text-align: center; } + +main, body > footer { + display: block; /* Just in case */ + max-width: 78ch; + margin: auto; +} + +img { max-width: 100%; } + +/* Lists */ + +ul, ol, dd { padding: 0 0 0 3ch; } +dd { margin: 0; } + +nav ul { + padding: 0; + list-style-type: none; +} +nav ul li { + display: inline; + padding-left: 1ch; + white-space: nowrap; +} +nav ul li:first-child { padding-left: 0; } + +/* Tables */ + +table { + width: 100%; + border-collapse: collapse; + overflow-x: auto; +} + +th, td { + border-width: 1px; + padding: 0 0.5ch; +} + +/* Copy inline */ + +a { text-decoration: none; } + +mark { + padding: 1px; +} + +code, samp { + border-width: 1px; + border-radius: 2px; + padding: 0.1em 0.2em; + white-space: nowrap; +} + +form, .card { + background: white; + box-shadow: 0 0 25px rgba(0,0,0,0.5); + padding: 0.5em; + margin: 1em 0; +} + +button, input { + font-size: 1.2rem; + margin: 0.5em; + padding: 0.25em 0.5em; +} + +dl { + padding: 0; + display: flex; + flex-wrap: wrap; +} + +ul { + padding: 0; + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; +} + +li { + list-style: none; +} + +dt, label { + display: inline-block; + width: 30%; + text-align: right; + margin-right: 1em; + font-weight: bold; +} + +dd, input { + display: inline-block; + min-width: 40%; + flex: 1; + text-align: left; +} + +#login-form { + display: flex; + justify-content: center; +} + +#logout-btn { + position: fixed; + right: 1em; + top: 1em; +} + +.film.card { + width: 960px; +} + +.film .poster { + float: left; + margin: 0 0.5em 0.5em 0; +} + +.film .title { + font-size: 2rem; +} + +.rating { + color: gold; + text-shadow: 1px 1px 1px rgba(0,0,0,0.5) +} + +app-film { + display: flex; + width: 960px; +} \ No newline at end of file diff --git a/swift-part1/index.html b/swift-part1/index.html new file mode 100644 index 0000000..bd548b5 --- /dev/null +++ b/swift-part1/index.html @@ -0,0 +1,37 @@ + + + + + + + + + Swift (part 1) | iOS Training + + + + + ++ + + diff --git a/swift-part2/index.html b/swift-part2/index.html new file mode 100644 index 0000000..2e68240 --- /dev/null +++ b/swift-part2/index.html @@ -0,0 +1,75 @@ + + + + + + + + +Swift (part 1)
Estimated time
1/2 day
Swift is the official programming language for developing iOS, iPadOS, macOS, watchOS and AppleTV apps. It can also target other platforms such as Windows, Linux and Android.
The source code of the language toolchain is hosted in swiftlang/swift GitHub repository.
A quick tour of some features
Swift has modern and interesting features. Here are some notable ones:
- Swift is statically typed and supports implicit typing.
- Static typing: types cannot change on runtime (it is the opposite of dynamic typing).
- Implicit typing: the compiler can infer the type whenever possible.
var
creates mutable variables.let
creates immutable variables or constants.- String interpolation is available with this syntax
\(expression)
.- Parenthesis are not required in
if
,for
,while
andswitch
statements.if
andswitch
statements are expressions.- for-each is the only type of for loop available.
- Optionals allows to write code free from null pointer errors (also called Null Safety in other languages).
- Functional programming is supported (Higher-order functions and functions as 1st class items, etc.).
- Object oriented programming is supported.
- Interfaces are called protocols and they are used a lot.
- Structures are available and provide a lot of features (More on that later).
this code illustrates some of the features listed above.
In the following sections, we will delve into more features.
++ and -- are removed since swift 3
This post details all the problems related to using these operators.
Functions
In the this section, the terms argument and parameter are used interchangeably.
The declaration of functions in Swift has the following peculiarities:
- Parameters are named and ordered. This means that when you call a function, you must specify the name of the arguments in the same order as the declaration.
- A parameter can have different external and internal names by declaring it like this:
externalName internalName: Type
. The external name is also called an argument label.- You can make a parameter anonymous by setting this external name:
_
.- Arguments can have a default value. These are also called optional arguments.
This code illustrates the above features.
Swift allows to use functions as first class items or citizens. This allows to store function references into variables, pass functions as arguments to other functions and return a function from a function. Here is a brief listing of the these features:
- A function can be assigned to a variable, passed as a function parameter or returned from a function.
- A function type can be expressed as follows:
(typeOfParam1, typeOfParam2, etc) -> returnType
.- The empty return type is
Void
.- We can use
typealias
to shorten writing long types.- Swift supports anonymous functions (also called lambda function) with the following syntax
{ argName1, argName2, etc. in // code }
This code illustrates these features.
Let's explore in the next section, one of the most amazing features of Swift which is Optionals.
Optionals (aka. Null safety)
In a nutshell, optionals is a compiler feature that allows you to avoid the infamous Null pointer exception or npe. The Swift compiler provides null safety and reports errors and warnings when we manipulate nullable (also called optional) values. Here is a list of null safety features provided by swift:
The name of null in iOS development
In Swift, the null value is called
nil
- All types are non optional by default. This means that we cannot assign
nil
to a variable or an argument. For example, this code failsvar s: String = nil
.- A type can be made optional by suffixing it with a ?. For example:
var s: String? = nil
.- You cannot call a method or a property of an optional type, unless you do one of those possibilities:
- Use optional chaining with the ? suffix.
- Provide a default value with the ?? operator.
- Unwrap the optional so that it becomes non optional.
- Force unwrap the optional using the ! suffix. This should never be used as it bypasses compiler checks.
Never unwrap with !
You must never force unwrap with the !. Use other unwrapping techniques instead. On of the rarest exceptions is with Interface builder's Outlets in UIKit
@IBOutlet var label: UILabel!
. Fortunately, since we are not using UIKit in this training, we will avoid this situation.This code illustrates null safety and how to use optional types.
Enumerations
Enumerations allow to work with a group of values in a type-safe fashion. Swift provides many interesting features to enumerations:
- When the compiler can infer it, you can omit the name of the enumeration when you use one of its values.
- Switch statements support enumerations.
- You can easily iterate over an enum's values by using
: CaseIterable
.- You can associate values or provide a raw value to enumeration cases. Raw values can be implicitly assigned.
- You can use another enumeration as associated value, this is called recursive enumeration.
This code illustrates some enumeration features. For further reading please consult the official documentation.
Error management
Swift provides two ways for error management: Exceptions and the Result type.
- Exceptions provide an alternate return route with the
throw [value]
keyword.
- The thrown value must conform the
Error
protocol. We can even throw a String that way.- We must call
throw
when we want to return an error. Throwing in a normal situation is a bad practice.- We say that a function throws when it can
throw
and exception. It must have thethrows
qualifier.- When we call a function that throws, we must precede the call with
try
keyword- When we call a function that throws, we can either propagate its error if it is thrown or handle to stop its propagation.
- The
Result
type is a an enum that has two possible cases:success(Sucess)
orfailure(Failure)
- The failure value must conform to the
Error
protocol- A
Result
can be handled with usual Swift features for enums:guard
,switch
, etc.- The
Result
type has can be used with the exception style. Itsget()
method returns the success value or throws the error.This code illustrates error handling features.
Some features in bulk
- Swift has 3 collection types out of the box: Array, Dictionary and Set. They are illustrated in this code
- We can use tuples in Swift as shown in this example
Exercises
Exercise 1
Please click on this link to view the exercise
Please open to see the solution(s)
Exercise 2
Please click on this link to view the exercise
Please open to see the solution(s)
Exercise 3
Please click on this link to view the exercise
Please open to see the solution(s)
Sources
Swift (part 2) | iOS Training + + + + + ++ + + diff --git a/to-go-further/index.html b/to-go-further/index.html new file mode 100644 index 0000000..931902b --- /dev/null +++ b/to-go-further/index.html @@ -0,0 +1,44 @@ + + + + + + + + +Swift (part 2)
Estimated time
1/2 day
Object oriented programming features
Swift supports most Object Oriented Programming features:
- Classes that can be instantiated into objects.
- Constructors and destructors are called initializers and deinitializers respectively.
- Encapsulation and 4 access levels that range from private to public
- Simple inheritance of classes. Multiple inheritance of classes and is not supported.
- Inheritance allows one class to use the characteristics of another.
- Method overriding and polymorphism and access control.
- Overloading of operators and functions, composition.
- Static methods and properties are supported.
- Generic types are supported
- Protocols which are the equivalent of Interfaces.
- Classes and structs can conform to multiple protocols.
- Protocols can have associated types which is similar to generic types.
- They are used a lot by swift developers to the point that there is a programming technique called Protocol oriented programming.
Here are some additional features:
- Extensions allow to add functions and conform to additional protocols outside of the original class, struct or protocol declaration. This has many uses that simplify our code and here are some examples.
- They can add methods to classes from any library that we can use.
- They can define default implementations in protocols.
- Abstract classes are not available
this code illustrates some of the above features.
In additions to classes, structs in swift are powerful and provide similar features than classes with some exceptions.
Structs
In Swift, structs have many similar features with classes. They support properties, methods, subscripts, initializers, extensions and conforming to protocols. The features that are only available in classes are as follows:
- Inheritance.
- Type casting (enables you to check and interpret the type of a class instance at runtime).
- Deinitializers.
- Reference counting allows more than one reference to a class instance (similar to pointers but much less complex to use).
this code sample shows how to use structs with protocols.
Opaque types
This feature seems advanced to understand but since it's used a lot in SwiftUI, let's explore a simple explanation and we'll provide some links to study it further.
In a base level, opaque types allow to return Protocols while keeping the concrete type information known by the compiler. It is enabled by prefixing the type with the
some
keyword.Opaque types allow to keep the benefits of abstracting the code on a developer level while maintaining the performance and optimization benefits of concrete typing. In addition to that, they allow the compiler to better handle some cases such as Self or associated type requirements. Please note that explaining all the features that opaque types bring to the code is an advanced topic. For more information and details, please read the articles mentioned in the Sources and more reading section.
For this training, we'll assume that opaque help types the compiler perform better optimizations with protocols, are used in many places in SwiftUI and allow to improve our code in some cases. We'll show below a simple use case where we can define a method that returns an
Equatable
.// Source: https://www.educative.io/answers/what-is-opaque-type-in-swift + +// create a function that returns some Equatable +// The compiler fails is the return type is just "Equatable" +func makeInteger() -> some Equatable{ + Int.random(in: 0...10) +} + +let firstInteger = makeInteger() +let secondInteger = makeInteger() + +// this returns "false" because they are of the same concrete type else, Xcode will scream at us. +print(firstInteger == secondInteger) + +func makeString() -> some Equatable{ + "A String" +} +let firstString = makeString() + +// Compiler error because the concrete type is not the same. +print(firstInteger == firstString) +
As of Swift 5.1 opaque types are only available for return values. As of Swift 5.7 opaque arguments have been implemented
Use structs by default
As surprising as it seems, Apple recommends using structs by default instead of classes. More precisely, when we want to add a new data type, we should not assume that it should be a class by default and check if a structure is more relevant. Apple provides the following recommendations:
- Use structures by default.
- Use classes when you need Objective-C interoperability.
- Use classes when you need to control the identity of the data you’re modeling.
- Use structures along with protocols to adopt behavior by sharing implementations.
We note that structures are the default choice mostly because they are value types. This makes the code more predictable because changes cannot come from a parent call. Another advantage of structs is that they are more friendly with functional programming. We'll talk about functional programming in the next section.
Functional programming features
Functional programming revolves around three main concepts: pure functions, immutable objects and declarative programming.
Pure functions are functions that do not have side effects and will thus return always the same output given the same input. Swift allows to create pure functions but does not provide compile time guarantees that a function is pure.
Immutable objects can be created using classes or structs with constant properties (declared with
let
). As mentioned above, structs are recommended by default and here are other good reasons. One of the most notable ones is that since structs are passed around by value, thus they help us avoiding side effects.Declarative programming can be easily explained as a way of programming that is centered around telling what to do and not how to do it. This allows to obtain a clearer and more maintainable code than traditional imperative programming. For example, when we want to filter a table, a for loop is not declarative (we say imperative in this case) while the WHERE SQL syntax is considered declarative. Declarative programing is possible in Swift through chaining functions and passing functions as arguments. Indeed, as we have seen earlier, Swift has 1st class support for functions. In addition to that, we can find declarative APIs in the standard Swift library and in Swift UI. The latter will be explored in a different chapter. For now, let's illustrate with this code how to process a list of strings using only declarative APIs provided by Swift.
Swift has many more features and provides a rich standard library. We'll explore them as needed in the next sections. For now, let's create some UIs in the next chapter.
Structured Concurrency
- Swift supports writing concurrent code in a structured way.
- Concurrency means that we execute multiple tasks at the same time. For example, update the UI while the app performs an HTTP request to the server.
- In Swift, we can create concurrent Tasks with the
Task
,TaskGroup
types.- Without Structured concurrency, we would use complex concepts to such as callbacks which make code less readable (have you lived the callback hell ?).
- We say that a code is structured when we use the well-know control flow structures :if/then/else, loops, functions, lexical scopes for variables.
- Structured concurrency means that we write concurrent code using the usual control flow structures (as opposed to callback-based concurrent code)
- In Swift is possible through the
async
andawait
keywords.- When we
await
aTask
, the control flow will continue when it end without blocking theTask
orTaskGroup
on which it is launched.- A function that has uses the
await
keyword must be declared asasync
- To summarize
async and await
+Task
andTaskGroup
= Structured Concurrency- Continuations allow to convert callback code into async/await
This swift script shows a sample of using Task + async/await
This swift script shows a sample of using TaskGoup + async/await
This swift script shows a sample of TaskGoup cancellation
This swift script shows how to convert callbacks into async/await
Structured Concurrency in Playground Book
// Reference: https://stackoverflow.com/a/24066317 +import PlaygroundSupport + +//Playground does not stop at the end of the code +PlaygroundPage.current.needsIndefiniteExecution = true + +func sampleFunc() async { + print("sampleFunc") + try? await Task.sleep(until: .now + .seconds(2)) +} + +Task { + await sampleFunc() + print("done") + // End the playground + PlaygroundPage.current.finishExecution() +} +
Generics
- Generics allow to pass a type as a parameter to a class, struct, enum or function.
- A type parameter can be declares with
<T>
whereT
is the type parameter.- Examples
func printArray<T>(array: [T]) { for item in array { print(item) } }
- Swift can infer the type of the parameter if it is not provided and if it's not ambiguous.
this code illustrates some of the above features.
Key-paths
- Key-paths allow to refer to properties of a type.
- They are created with the
\.propertyName
syntax.- They are often used to sort, filter, group and map collections and in SwiftUI to bind properties to UI elements.
this code illustrates some of the above features.
Exercises
Exercise 1
Please click on this link to view the exercise
Please open to see the solution(s)
Exercise 2
Please click on this link to view the exercise
Please open to see the solution(s)
Exercise 3
Please click on this link to view the exercise
Sources and more reading
Going further | iOS Training + + + + + ++ + + diff --git a/ui-development/index.html b/ui-development/index.html new file mode 100644 index 0000000..2ac9394 --- /dev/null +++ b/ui-development/index.html @@ -0,0 +1,64 @@ + + + + + + + + +Going further
Server side development
- Vapor is a Swift framework that allows to develop servers
- Install the Vapor cli
brew install vapor
- Create a vapor project
vapor new hello-vapor -n
- Run the server: cd
hello-vapor
andswift run
Building for debugging... +Build complete! (1.25s) +[ NOTICE ] Server starting on http://127.0.0.1:8080 +
- Test the server
➜ curl http://127.0.0.1:8080 +It works! +➜ curl http://127.0.0.1:8080/hello +Hello, world! +
Swift and SwoftUI on the browser
- SwiftWasm is a project that allows to run Swift code in the browser using WebAssembly.
- SwiftWasm Pad is an online editor that allows to write SwiftUI code and run it in the browser. It relies on the TokamakUI framework which is SwiftUI-compatbile framework(or a re-implementation of SwiftUI) for the web.
- SwiftWebUI is another project that allows to run a server that renders SwiftUI to the browser. Note that it is considered as a toy project by its creator.
Advanced Swift
Conclusion
This training was introduction on Swift and SwiftUI. It just scratched the surface of developing for the Apple development. There are many things that we didn't explore such as the accessibility, hardware features such as geolocation and the technical aspects of the developer account (certificates, provisioning profiles, etc.).
To go further, it is advised to watch the videos from Apple's WWDC (WorldWide Developer Conference - pronounced "dubdub dee cee"). There many other resources available online that you should pick and choose depending on the needs. Here are some of them:
UI development | iOS Training + + + + + ++ + + diff --git a/workbox-1ab968a5.js b/workbox-1ab968a5.js new file mode 100644 index 0000000..b46d3f7 --- /dev/null +++ b/workbox-1ab968a5.js @@ -0,0 +1 @@ +define(["exports"],(function(t){"use strict";try{self["workbox:core:7.0.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.0.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class o{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let o=r&&r.handler;const c=t.method;if(!o&&this.i.has(c)&&(o=this.i.get(c)),!o)return;let a;try{a=o.handle({url:s,request:t,event:e,params:i})}catch(t){a=Promise.reject(t)}const h=r&&r.catchHandler;return a instanceof Promise&&(this.o||h)&&(a=a.catch((async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n}))),a}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const o=r.match({url:t,sameOrigin:e,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&0===i.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let c;const a=()=>(c||(c=new o,c.addFetchListener(),c.addCacheListener()),c);const h={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},u=t=>[h.prefix,t,h.suffix].filter((t=>t&&t.length>0)).join("-"),l=t=>t||u(h.precache),f=t=>t||u(h.runtime);function w(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:7.0.0"]&&_()}catch(t){}function d(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),r=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:r.href}}class p{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class y{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.h.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.h=t}}let g;async function R(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=e?e(r):r,c=function(){if(void 0===g){const t=new Response("");if("body"in t)try{new Response(t.body),g=!0}catch(t){g=!1}g=!1}return g}()?i.body:await i.blob();return new Response(c,o)}function m(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class v{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}}const q=new Set;try{self["workbox:strategies:7.0.0"]&&_()}catch(t){}function U(t){return"string"==typeof t?new Request(t):t}class L{constructor(t,e){this.u={},Object.assign(this,e),this.event=e.event,this.l=t,this.p=new v,this.R=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.p.promise)}async fetch(t){const{event:e}=this;let n=U(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.l.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=U(t);let s;const{cacheName:n,matchOptions:i}=this.l,r=await this.getCacheKey(e,"read"),o=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,o);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=U(t);var i;await(i=0,new Promise((t=>setTimeout(t,i))));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(o=r.url,new URL(String(o),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var o;const c=await this.q(e);if(!c)return!1;const{cacheName:a,matchOptions:h}=this.l,u=await self.caches.open(a),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=m(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),o=await t.keys(e,r);for(const e of o)if(i===m(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?c.clone():c)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of q)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:a,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.u[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=U(await t({mode:e,request:n,event:this.event,params:this.params}));this.u[s]=n}return this.u[s]}hasCallback(t){for(const e of this.l.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.l.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.R.push(t),t}async doneWaiting(){let t;for(;t=this.R.shift();)await t}destroy(){this.p.resolve(null)}async q(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class b{constructor(t={}){this.cacheName=f(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new L(this,{event:e,request:s,params:n}),r=this.U(i,s,e);return[r,this.L(r,i,s,e)]}async U(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this._(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async L(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}class C extends b{constructor(t={}){t.cacheName=l(t.cacheName),super(t),this.C=!1!==t.fallbackToNetwork,this.plugins.push(C.copyRedirectedCacheableResponsesPlugin)}async _(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.O(t,e):await this.N(t,e))}async N(t,e){let n;const i=e.params||{};if(!this.C)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,o=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&o&&"no-cors"!==t.mode&&(this.k(),await e.cachePut(t,n.clone()))}return n}async O(t,e){this.k();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}k(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==C.copyRedirectedCacheableResponsesPlugin&&(n===C.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(C.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}C.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},C.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await R(t):t};class E{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.K=new Map,this.P=new Map,this.T=new Map,this.l=new C({cacheName:l(t),plugins:[...e,new y({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.l}precache(t){this.addToCacheList(t),this.W||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.W=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=d(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.K.has(i)&&this.K.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.K.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.T.has(t)&&this.T.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.T.set(t,n.integrity)}if(this.K.set(i,t),this.P.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return w(t,(async()=>{const e=new p;this.strategy.plugins.push(e);for(const[e,s]of this.K){const n=this.T.get(s),i=this.P.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}}))}activate(t){return w(t,(async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.K.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}}))}getURLsToCacheKeys(){return this.K}getCachedURLs(){return[...this.K.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.K.get(e.href)}getIntegrityForCacheKey(t){return this.T.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}let O;const x=()=>(O||(O=new E),O);class N extends i{constructor(t,e){super((({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const o=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some((t=>t.test(s)))&&t.searchParams.delete(s);return t}(r,e);if(yield o.href,s&&o.pathname.endsWith("/")){const t=new URL(o.href);t.pathname+=s,yield t.href}if(n){const t=new URL(o.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}}),t.strategy)}}function k(t){const e=x();!function(t,e,n){let o;if("string"==typeof t){const s=new URL(t,location.href);o=new i((({url:t})=>t.href===s.href),e,n)}else if(t instanceof RegExp)o=new r(t,e,n);else if("function"==typeof t)o=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}a().registerRoute(o)}(new N(e,t))}t.cleanupOutdatedCaches=function(){self.addEventListener("activate",(t=>{const e=l();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter((s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t));return await Promise.all(s.map((t=>self.caches.delete(t)))),s})(e).then((t=>{})))}))},t.clientsClaim=function(){self.addEventListener("activate",(()=>self.clients.claim()))},t.precacheAndRoute=function(t,e){!function(t){x().precache(t)}(t),k(e)}}));UI development
Estimated time
1/2 day
Apple provides two official UI frameworks : UIKit and SwiftUI.
UIKit is the originally used framework for UI development. It relies on defining the UI in a separate file (storyboard or xib) and the behavior in a swift file. In 2019, Apple release the first version of SwiftUI.
The remainder of this training focuses on SwiftUI.
SwiftUI
SwiftUI brings a new approach to build UIs that we can summarize as follows:
- All the UI is defined in Swift code. Neither Storyboards nor xibs are needed anymore.
- The UI is defined in a declarative style.
- States and bindings allow to hold the app data. The app UI updates automatically when these data change.
- UI elements are structs that conform to the View protocol.
- Complex views can be defined by breaking them into smaller views. This is called view composition.
- The modifier technique is used to apply modifications to a view. A modifier returns a new view each time.
The official documentation of SwiftUI is available here.
Prerequisites
It is recommended to use Xcode to learn and create SwiftUI apps. For simple apps, we can use the Swift Playgrounds app. There is a web playground that can be exceptionally used. You can see a screenshot of the tool below.
Another promising alternative to watch is compnerd's windows port of UIKit and SwiftUI. So, if you can have a recent version Xcode running, this is be the best IDE for SwiftUI development.
Anatomy of a basic view
The following code shows a sample view.
struct ContentView: View { + var body: some View { + VStack { + Text("Hello SwiftUI") + .font(.largeTitle) + .foregroundColor(.blue) + .padding() + Button(action: {}) { + HStack { + Image(systemName: "suit.heart.fill") + .foregroundColor(.red) + Text("I am a button") + .font(.headline) + .foregroundColor(.white) + } + .padding(12) + .background(Color.orange) + .cornerRadius(8) + } + } + } +} +
As noted earlier, SwiftUI views are structs that conforms to the View protocol. This protocol defined a computed property that returns a View as an opaque type.
The body of the view has a
VStack
as its root element. AVStack
is a container view that arrange its direct children vertically (on a column). The first child is aText
view and its second child is aButton
.The
Text
view chains calls to some methods that we call modifiers. They allow to do anything that we want to the view that called it and they return a newView
instance. This means that we can apply another modifier to the result of a modifier and so on (this is called chaining). This allows modifiers to have a declarative syntax that makes the code easy to understand. SwiftUI provides built-in modifiers and allows us to create custom ones. Can you match all the modifiers used in the code and their effects ?The modifiers used are:
font(...) +foregroundColor(...) +padding(...) +background(...) +cornerRadius(...) +
The button has no action, meaning that is does nothing on click and its content is defined as an
HStack
. AnHStack
is a container view that arrange its direct children horizontally (on a row). The stack contains an image and a button.The view renders as illustrated by the image below.
Let's do more SwiftUI.
A summary of important concepts
@State
: Single source of truth of a view and should not be shared with other views.@Binding
: allows to pass a reference of a state to a child view using$state
.@EnvironmentObject
: Allows to globally share data between views. An@EnvironmentObject
conforms to theObservableObject
protocol and its properties have the@Published
property wrapper.@ObservedObject
: Allows to observe changes in an object that conforms to theObservableObject
protocol.PW: complete some official SwiftUI tutorials
Apple provides a comprehensive SwiftUI tutorial that covers most of the basic use cases such as creating views and handling inputs, animations and transitions.
Please cover these tutorials to get a good grasp of SwiftUI.
- Basic layout
- Creating and combining views (40 min)
- Building lists and navigation (35 min)
- Handling user input (20 min)
- Animations and complex layouts
- Animating views and transitions (20 min)
- Composing complex interfaces (20 min)
- Working with UI controls (25 min)