
اكتسبت Apple Watch شعبية بسرعة وأصبحت الساعة الأكثر شهرة في العالم ، قبل Rolex وغيرها من الشركات المصنعة. كانت فكرة إنشاء تطبيق للساعات في مكتب 2GIS منذ عام 2015.
أمامنا ، أصدرت Apple نفسها فقط تطبيقًا كاملًا مع بطاقة على الساعة. يعرض تطبيق Yandex.Map أدوات حركة المرور ووقت السفر إلى المنزل والعمل فقط. Yandex.Navigator وخرائط Google و Waze و Maps.Me غير متوفرة بشكل عام على الساعة.
في الواقع ، نظرًا لقيود النظام العديدة وتعقيد التطوير ، إما أن الشركات لا تقدم تطبيقات مراقبة على الإطلاق ، أو تجعلها بسيطة للغاية. لا يمكنك فقط التقاط ورسم خريطة على الساعة. لكننا استطعنا.
ألق نظرة خاطفة تحت القطة لتكتشف كيف نما مشروع الحيوانات الأليفة إلى منتج كامل.
محدث: https://github.com/teanet/DemoWatch
قررنا عمل خريطة. ماذا كان في البداية؟
- تجربة تطوير على الساعة - يومان من العمل في مشروع اختبار.
- تجربة SpriteKit - 0 يوم.
- تجربة كتابة MapKit - 0 يومًا.
- شك في أن شيئًا ما قد يحدث خطأ - ∞.
التكرار 1 - هروب الفكر
نحن أناس جادون ، لذلك قررنا في البداية أن نضع خطة عمل. أخذنا في الاعتبار أننا نعمل في سباق سريع التخطيط جيدًا ، ولدينا خمس نقاط قصة لـ "مهام المنتجات الصغيرة" والجهل الكامل بمكان البدء.
الخريطة هي صورة كبيرة جدًا. يمكننا عرض الصور على مدار الساعة ، مما يعني أنه يمكننا التعامل مع عرض البطاقة.
لدينا خدمة يمكن أن تقطع البطاقة إلى قطع:
إذا قطعت مثل هذه الصورة ووضعت WKImage ، فإننا نحصل على أبسط نموذج أولي للعمل بخمسة سنتات.
وإذا أضفت PanGesture إلى هذه الصورة وقمت بتثبيت صورة جديدة في كل تمريرة ، نحصل على محاكاة للتفاعل مع البطاقة.
/ ابتهج / يبدو الأمر فظيعًا ، يبدو متشابهًا ، إنه يعمل بشكل أسوأ ، ولكن في الواقع المهمة قد اكتملت.
التكرار 2 - النموذج الأولي الأدنى
تنزيلات الصور المستمرة مكلفة للبطارية في ساعات. نعم ، ويعاني وقت التمهيد نفسه. أردنا الحصول على شيء أكثر اكتمالا واستجابة. من زاوية الأذنين ، سمعنا أن الساعة تدعم SpriteKit - إطار WatchOS الوحيد القادر على استخدام الإحداثيات والتكبير / التصغير وتخصيص كل هذا الروعة بنفسك.
بعد ساعتين من StackOverflow Driven Development (SDD) نحصل على التكرار الثاني:
واحد SKSpriteNode ، واحد WKPanGestureRecognizer.
/ افرحوا / نعم هذه MapKit لـ 6 كوبيل ، تعمل بشكل كامل. الإفراج العاجل!
التكرار 3 - إضافة البلاط والتكبير
عندما كانت المشاعر نائمة ، تساءلوا إلى أين يذهبون بعد ذلك.
يفهم أن أهم شيء:
- استبدل الصورة بالبلاط.
- إرفاق 4 مربعات بحزمة التطبيق وربطها معًا.
- توفير صور تكبير / تصغير.
دعنا نسقط 4 مربعات في حزمة التطبيق ، ثم نضعها في مربع معين:
let rootNode = SKSpriteNode()
بمساعدة الرياضيات البسيطة ، سنربطها معًا.
نقوم بالتكبير من خلال WKCrownDelegate:
internal func crownDidRotate( _ crownSequencer: WKCrownSequencer?, rotationalDelta: Double ) { self.scale += CGFloat(rotationalDelta * 2) self.rootNode.setScale(self.scale) }
/ افرحوا / حسنًا ، هذا مؤكد الآن! زوجان من الإصلاحات ، وإلى السيد.
التكرار 4 - تحسين التفاعل مع الخريطة
في اليوم التالي ، اتضح أن أداة تثبيت SpriteKit لا تؤثر على التكبير. يتجاهل الزوم تمامًا نقطة الربط ويحدث بالنسبة إلى مركز rootNode. اتضح أنه لكل خطوة تكبير نحتاج إلى ضبط الوضع.
سيكون من الرائع أيضًا تحميل المربعات من الخادم ، بدلاً من تخزين العالم كله في ذاكرة الساعة.
لا تنس أنه يجب ربط البلاط بالإحداثيات بحيث لا تقع في وسط SKScene ، ولكن في الأماكن المناسبة على الخريطة.
يبدو البلاط مثل هذا:

يحتوي كل مستوى تكبير / تصغير (يشار إليه فيما يلي بـ "z") على مجموعة من البلاط الخاصة به. بالنسبة z = 1 ، لدينا 4 مربعات تشكل العالم كله.

for z = 2 - من أجل تغطية العالم كله ، تحتاج بالفعل إلى 16 قطعة من البلاط ،
ل z = 3-64 بلاطة.
ل z = 18 ≈ 68 * 10 ^ 9 بلاطات.
الآن يجب وضعهم في عالم SpriteKit.
حجم البلاط الواحد 256 * 256 نقطة ، مما يعني
بالنسبة إلى z = 1 ، سيكون حجم "العالم" 512 * 512 نقطة ،
بالنسبة إلى z = 2 ، سيكون حجم "العالم" يساوي 1024 * 1024 نقطة.
لسهولة الحساب ، ضع البلاط في العالم على النحو التالي:

ترميز البلاط:
let kTileLength: CGFloat = 256 struct TilePath { let x: Int let y: Int let z: Int }
حدد إحداثيات البلاط في مثل هذا العالم:
var position: CGPoint { let x = CGFloat(self.x) let y = CGFloat(self.y) let offset: CGFloat = pow(2, CGFloat(self.z - 1)) return CGPoint(x: kTileLength * ( -offset + x ), y: kTileLength * ( offset - y - 1 )) } var center: CGPoint { return self.position + CGPoint(x: kTileLength, y: kTileLength) * 0.5 }
الموقع مناسب ، لأنه يسمح لك بإحضار كل شيء إلى إحداثيات العالم الحقيقي: خط العرض / خط الطول = 0 ، والذي يقع في وسط "العالم".
يتم تحويل خط العرض / خط الطول للعالم الحقيقي إلى عالمنا على النحو التالي:
extension CLLocationCoordinate2D { // ( -1 < TileLocation < 1 ) func tileLocation() -> CGPoint { var siny = sin(self.latitude * .pi / 180) siny = min(max(siny, -1), 1) let y = CGFloat(log( ( 1 + siny ) / ( 1 - siny ))) return CGPoint( x: kTileLength * ( 0.5 + CGFloat(self.longitude) / 360 ), y: kTileLength * ( 0.5 - y / ( 4 * .pi ) ) ) } // zoomLevel func location(for z: Int) -> CGPoint { let tile = self.tileLocation() let zoom: CGFloat = pow(2, CGFloat(z)) let offset = kTileLength * 0.5 return CGPoint( x: (tile.x - offset ) * zoom, y: (-tile.y + offset) * zoom ) } }
مع مشاكل مشوش التسوية التكبير. اضطررت لقضاء يومين من الراحة لتجميع الجهاز الرياضي بأكمله وضمان الدمج المثالي للبلاط. أي أن البلاطة لـ z = 1 يجب أن تذهب بشكل مثالي إلى أربعة بلاطات لـ z = 2 والعكس صحيح ، فإن البلاطات الأربعة لـ z = 2 يجب أن تذهب إلى بلاطة واحدة لـ z = 1.

بالإضافة إلى ذلك ، كان من الضروري تحويل الزووم الخطي إلى تكبير أسي ، حيث تختلف الزوم من 1 <= z <= 18 ، ومقاييس الخريطة مثل 2 ^ z.
يتم توفير الزوم السلس عن طريق ضبط موضع البلاط باستمرار. من المهم أن يتم خياطة البلاط بالضبط في المنتصف: أي أن بلاط المستوى 1 ينتقل إلى 4 بلاط المستوى 2 بتكبير 1.5.
يستخدم SpriteKit عوامة تحت غطاء المحرك. بالنسبة إلى z = 18 ، نحصل على انتشار للإحداثيات (-33 554 432/33 554 432) ، ودقة التعويم 7 بت. عند الخروج ، لدينا خطأ في منطقة 30 نقطة. لتجنب حدوث "فجوات" بين النصفين ، نضع البلاط المرئي بالقرب من مركز SKScene قدر الإمكان.
/ ابتهج / بعد كل هذه الإيماءات ، حصلنا على نموذج أولي جاهز للاختبار.
تاريخ الإصدار
نظرًا لأن التطبيق لم يكن يحتوي بالفعل على المعارف التقليدية ، فقد وجدنا اثنين من المتطوعين لإجراء القليل من الاختبار. لم يجدوا مشاكل خاصة ، وقرروا أن ينحرفوا إلى الجانب.
بعد الإصدار ، اتضح أنه على مدار الساعة من السلسلة الأولى ، لم يكن لدى المعالج الوقت لرسم الإطار الأول للبطاقة في 10 ثوانٍ وسقوط المهلة. اضطررت في البداية إلى إنشاء بطاقة فارغة تمامًا لتناسب 10 ثوانٍ ، ثم تحميل الركيزة تدريجيًا. قم أولاً بتطوير جميع مستويات الخريطة - ثم قم بتحميل البلاط لها.
استغرق الأمر الكثير من الوقت لتصحيح الشبكة ، وتكوين ذاكرة التخزين المؤقت بشكل صحيح وبصمة ذاكرة صغيرة حتى لا يحاول WatchOS إنهاء تطبيقنا لأطول فترة ممكنة.
بعد تعريف التطبيق ، اكتشفنا أنه بدلاً من المربعات المعتادة ، يمكنك استخدام مربعات Retina ، بدون أي ضرر تقريبًا لوقت التحميل واستهلاك الذاكرة ، وفي الإصدار الجديد الذي تحولوا إليه بالفعل.
النتائج والخطط للمستقبل
يمكننا بالفعل عرض مسار على الخريطة باستخدام المناورات المضمنة في التطبيق الرئيسي. ستكون الميزة متاحة في أحد الإصدارات القادمة.
أثبت المشروع ، الذي بدا مستحيلًا في البداية ، أنه مفيد للغاية بالنسبة لي شخصيًا. غالبًا ما أستخدم التطبيق لفهم ما إذا كان الوقت قد حان للانطلاق عند المحطة المناسبة. أعتقد أنه في فصل الشتاء سيكون أكثر فائدة.
في الوقت نفسه ، كان مقتنعا مرة أخرى بأن تعقيد المشروع ، وإيمان الآخرين بنجاح المهمة أو توفر وقت الفراغ في العمل ، ليست مهمة للغاية. الشيء الرئيسي هو الرغبة في عمل مشروع وحركة تدريجية مملة نحو الهدف. نتيجة لذلك ، لدينا MapKit كاملة ، والتي تكاد تكون غير محدودة وتعمل مع 3 WatchOS. يمكن تعديله كما يحلو لك دون انتظار قيام Apple بطرح واجهة برمجة التطبيقات المناسبة للتطوير.
PS للمهتمين ، يمكنني وضع مشروع منتهي. مستوى الكود هناك بعيد عن الإنتاج. ولكن ، وفقًا للمبدأ العسكري ، لا يهم كيف يعمل ، الشيء الرئيسي هو أنه يعمل!