Posts

Introducing TwitterText Swift library

TwitterText Logo

From start of September ‘20 I was working on connecting my Code Conf app’s backend to Twitter API. Soon I got to the point where I have to properly calculate tweet lengths where they can consists from ASCII characters but also can contain emoji’s. This led me to read deeper through Twitter API docs where I found that there is available library making those calculations on much bigger range of Unicode characters than I even planned to use.

This set of libraries were written in many languages (including Objective-C) but not in Swift. As Code Conf backend API is hosted on Linux I needed Swift Package Manager compatible library for it.

I got two options:

  • Try to calculate tweet lengths for myself, and almost for sure reinvent broken wheel, or
  • Learn to read and understand Objective-C code and try to transcode this library to Swift.

I have chosen the latter option and decided that I will at least get new knowledge which will eventually help me in future of my iOS developer journey. I reminded myself that I probably have a book from which I can learn Objective-C. I wasn’t wrong. I found that I own book by Paul Hudson “Objective-C for Swift Developers” which I have bought last year as a part of Paul’s Swift Power Pack.

I have spent 2 days spent on reading it and felt ready to test my knowledge in practice. I have cloned twitter-text Objective-C library, created Swift Package Project and started transcoding Objective-C code to Swift.

I was tweeting a lot about this (insane for me then) idea of learning Objective-C in 2 days ;) At that time Rizwan contacted me on Twitter with question if I need some help on this project. I wasn’t sure, but felt that some kind of consultancy will be blossom when I will be stuck in some parts. It took me to 2 weeks to make this code compile and pass test suite on both macOS and Linux.

During development we found that Objective-C library uses IDNSDK library written in C. We have written Swift wrapper to this library and now it’s possible to use it also as Swift Package Manager dependency.

I was able to create most of the code by myself but finishing this library would not possible with lots of consultancy, knowledge sharing and huge amount of fixes provided by Rizwan. Especially for those parts where conversion of Objective-C code to Swift required proper type casting.

Thank you Rizwan a lot for your time and help. At this point I would like to also say thank you to authors of Objective-C version of twitter-text library: Satoshi Nakagawa, David LaMacchia and Keh-Li Sheng. Without their work I would not have a base upon which I have made this Swift library.

This was great time to extend my knowledge in many ways. 3 weeks ago I didn’t even thought that I will be working with Objective-C code and C code was totally out of my mind.

I’m pleased to share Swift twitter-text library with you.

The Swift implementation of the twitter-text parsing library, which allows you to parse Tweets and calculate length, validity, parse @mentions, #hashtags, URLs, and more.

Both libraries are open source and licensed with MIT License.

You can get them on Github:

PS. now I can go back to work further on connecting Code Conf backend to Twitter API :)

#Server Side, #SPM, #Open Source, #Twitter, #twitter-text

Code Conf - The long journey

This is a story of creating Code Conf app. Take good coffee or tea. Sit well and I hope you will enjoy it. It was awesome retrospective to me.

It was few weeks when dust after launching my first app Family Graves Map has settled. I started to think about an idea for my next project.

I have had few candidates onboard but one has been a leader before the rest. At that time when I was releasing my debut iOS app, I was learning iOS development for only 6 months. I was learning a lot from different sources: articles on the web, Stanford C193P course, books and conference videos. I have found that the last category of my knowledge source as the hardest one to discover and find. I didn’t know what are names of iOS developer conferences, I only found a few of them at the start of my iOS developer journey. That was the case … there are lots of people like me who want to learn, get inspired by others but have no idea where to find this knowledge. Maybe they find as me just one or two conferences and then struggle with not knowing the others. This idea was so strong in my mind that I decided to go with it and tried to make and app from it. I created some drafts on paper with main features, how I want it to look, what data I need to have and how the user could interact with this data.

At first, this looked like an easy project. Three main data models, simple API and I will end it on 02.02.2020. I thought this date was great to launch this project. But this was on November 10th 2019 when I committed first lines code for API. Life has written its own story during this project.

If you are for the first time on my site, maybe not yet know me and have not read my bio I will share with you one detail. I am a full-time Pharmacy Manager. My daily job is far away from coding. I was coding after hours, on weekends, sharing with this project my family time.

UIKit or SwiftUI?

While making this first drafts I have decided to try to write iOS part of this project in brand new SwiftUI framework. After reading lots of resources I felt that this will be a good idea to try. In a few days, I was able to create the first static draft of almost the whole UI I had described then on paper. It was easy, it was fast, and it gave me a lot of fun to do it. I was sure that with no so huge experience with UIKit I won’t be able to prepare all these things with the same amount of time and struggling with storyboards and auto layout.

Decision number 1: SwiftUI no matter what will happen next.

Backend API framework

iOS app was just one side of my project. The second one which I knew will be required was backend API. I was programming occasionally in PHP a few years back in Symfony framework and done medium size backoffice app in it, but I wasn’t sure if I want to learn one more time “new paradigm” with the current version of Symfony and PHP. Back around WWDC 19, I have bought a set of books from Paul Hudson named “Swift Platform Pack”. It contains books “Server-Side Swift” Kitura and Vapor editions. I thought that writing my backend in Swift will be a lot easier than learning at the same time PHP for backend and Swift for iOS part. After research, I decided to go with Vapor. I deep-dived into docs, above mentioned book and started my first tests how it worked. After a few weeks, I have read that IBM had abandoned Kitura project. I was more than happy with my former choice. At the time I started my project current stable version of Vapor was Vapor 3.

And here we are with decision number 2: Vapor as backend API

Data Storage

There was one more decision I should have made at the start of my project but I have made it later. I have no idea why? Maybe because of the easier “entry point” maybe because of toolkit around. This was where to store my data. I was more than sure that I cannot go with SQLite and I went with MySQL and more detailed with its fork MariaDB. Later on, as I get known more with Vapor, how it works and behaves after some recommendations from Vapor’s Discord server (sorry I do not remember who first recommended this to me) I changed my backend storage to PostgreSQL on 10th of February ‘20.

We have now 3rd decision about the foundations of my project. There will be one more but more about it later on.

How would I call it?

Having a good name for every product is important. The goal was to make it unique, recognizable and easy to remember or spell. I tried mixing words: talks, speeches, videos, developer, development and similar. I used their shorter versions but I wasn’t happy with any result I got.

Then came an idea. What are talks all about? They are about coding, they are given on conferences. And I got draft “coding conferences”. It was too long but into the point with the purpose of my app. The path from this point to the final name of “Code Conf” was short. I like it very much. It’s short, it’s catchy. And from the 12th of December ‘19, my new project had its name.

Road to deliver it

After creating initial drafts of UI I have focused on coding backend, designing routes and their purpose, communication with the database. I was using at first Vapor’s Fluent ORM for communication with the database. Unfortunately, I found very fast that it is limiting me while I was creating more complex queries with dependencies between tables. On Vapor’s Discord, I got suggestion by iMike to try his SwifQL package which is DSL for SQL queries written in Swift. Having a background in working with SQL from the times I was coding in PHP I decided to give it a try. And this was a good decision. I could then prepare responses based on multiple objects without unneeded nesting of dependencies.

From the start of my project I was following one man in the Swift community and learned from his brand new blog posts a lot. He was kind so many times to share his free time helping me resolve many of my development struggles. That man is one and only Donny Wals. I was looking for a way to communicate my iOS app with my backend and how to make this code reusable. My own written solution was working but had a huge amount of duplicated code in it. After I read his robust networking post and with his advice, I have rewritten it and made this code clean and reusable. Adding new endpoints to my API was a pleasure from this point.

Time was going, lines of code were growing until the point when I had to implement authorization of my app in backend API. As I decided that I will go with an anonymous account as the base type of account I had to implement keeping user authenticated and at the same time my backend secure. I was testing the implementation of token refreshing with completion handler based networking layer. This made my code complicated and hard to maintain with multiple levels of nested completion handlers. And here we go again … Donny had come to the rescue, yes that Donny had completed this series of blog posts about Combine - and in particular one on refactoring his previous networking implementation to Combine and his Practical Combine book. Having this knowledge I tried to recreate my implementation of the networking layer in Playgrounds using Combine. It wasn’t easy and straightforward to make it work because reactive programming was new to me. But it started to work and as a positive side effect, I got my authentication working in a way I wanted it to work.

From the start, I knew that my app will be free to download and use but it will contain premium features which will require purchasing. I have read case studies on Twitter where people were implementing their IAP’s lightning fast. The solution was Revenue Cat. Having no experience with managing IAP’s I managed to implement them in my app in one evening on the 21st of April ‘20. And their generous pricing model where you only pay when you break a limit of 10k USD of tracked subscriptions per month or upgrade to a more advanced plan.

One week later I had first videos playing in-app, my API was deployed to Heroku and I could prepare first beta tests. But more on this later on.

Mastermind power

Few days before I was implementing my IAP’s someone from my iOS Dev list on Twitter had forwarded Dominic Williams` proposal to create iOS developer Mastermind Group. I got interested in this and decided to contact him to get more information about how this would look like. In about a week Dominic had completed our group with Ben Noland and we started to our first meetings. We are from different countries, from different timezones but we agreed to meet every other week. I would not tell you any details about what we are talking on them but joining this group had given me the power to keep going with my project. I felt obliged to show progress on every meeting, prepare new topics to discuss. Those discussions helped me in making decisions about topics I wasn’t sure which way to go. It is also a great way to share knowledge and experience. We all work on our products but we have one certain goal: To make our apps great, every in its own unique way. And I can say now that they had their impact on how my app looks now.

Back to coding

I have invited to my early bird TestFlight some of the very well known guys in our Swift community. I was pleased with their great opinions, little suggestions for improvements and general interest in my app. This made me feel that my app can to be useful to other developers. On my early bird list besides of Donny, Dominic and Ben I got also Dave Verwer, Benedikt Tertechte, Shihab, Pedro Carrasco, Vadim Bulavin, Mateusz Matrejek, Niv Ben-Porath, Christian Weinberger, Tibor Bodecks. Later on, I have invited to beta tests a few more people. Thank you all for taking time testing and reviewing my app on this early stage and later with every new build.

As the first tests were going on a side with the use of API on Heroku it was time for breaking my API at all for almost a week. I was postponing this move for a very long time. But lack of some features in SwifQL for Vapor 3 forced me to do it. I cannot say it was easy. It wasn’t at all. I started a migration to Vapor 4 and Bridges + SwifQL on 2nd of May ‘20. The migration took 7 days and resulted in more than 4000 lines of code changed. If I knew that this migration will require such amount of work beforehand I do not know if I would decide to do it. But better later than never on the 8th of May I got my API fully working with a new setup. I have learned a lot during this process. And where I struggled with errors iMike was so kind to help me on his Discord server. What’s more important every feature in SwifQL and Bridges which was missing to my use case he implemented it almost on the fly. If you ever will be working with Server Side Swift consider using his packages. They are worth trying.

Like almost every iOS app mine also is using User Defaults to store some settings. Isn’t this great to have them synced between your devices where one setting is propagated to all other you have my app installed? Inspired by Paul Hudson’s Unwrap app source code I tried to use Zephyr for syncing some of them. This required some additional work on Apple Developer portal but in one day I got them working. Working with User Defaults “the old way” wasn’t so great experience before Apple had announced Property Wrappers. And you know what? I learned next thing new to me by following Antoine van der Lee. He wrote a simple yet powerful solution for managing User Defaults with Property Wrappers. Adding to this combo my Enum with all UserDefaults names I got very solid and easy to extend code.

Near the end of May, I started getting disappointed with Heroku performance (mostly long pings and slow reaction to requests). I was looking sometime earlier for Vapor hosting solutions. I found Digital Ocean. What’s more Vapor docs are also covering deployment on Digital Ocean. 14 years of managing Linux servers was very helpful this time. I have deployed my API to Digital Ocean in one afternoon on the 31st of May.

Crash time

I could end this part of my journey saying only: “More code … more crashes”

But I will not leave you without this part of the story. Many devs who I found during working on SwiftUI were reporting problems and lack of its stability. I probably was happy enough to not get them for about 6 months. Of course, I got hit by minor bugs or problems but my app wasn’t crashing almost at all before June. Every week I was closer to the planned launch date, which I decided to be 18th of June. But having lots of crashes was worrying me. It was time to implement some crash reporting tool in my app. But which one to choose. I knew I would not use Crashlitics. During one of many chats with Donny, he said that they are using Sentry in his daily job. Sentry has some free plan so I decided to give it a try. And crash reports started appearing. Some of them were useful and helping me resolve a few issues. As it turned out many of them were not directly connected to my code. That crashes appeared in SwiftUI code, its renderer and memory management. This is what I was unable to fix. I decided to fix other things and wait for WWDC and fixes in the new version of SwiftUI.

The other thing I was trying to resolve was reusing the same view in two different places. In one place everything worked without issues, but on another View was losing its data, despite the fact that this data was supplied to it in both the same way. This led me to Vadim Bulavin’s post about implementing MVVM in SwiftUI. His example is covering whole MVVM architecture, but I didn’t have at that moment too much time to refactor whole logic to MVVM. I decided to try and use some basic concepts from his example. This fixed my issues with empty views instead of filled with data. I was getting disappointed with SwiftUI because of these tricks required. But there was no turning back.

Let’s launch it

On the 30th of May, I have posted my first mysterious tweet with my plan for launching. I wanted to gain some interest in my project without revealing any details then. It had created a huge interest in my project which made me more motivated to work harder heading launch date.

On the 14th of June, I have uploaded the first build for Apple review.

Result: Rejected

Why?

The first one was obvious. My error. When you are attaching IAP’s to App for the first time you have to define them in App’s general info tab. I didn’t know this. Easy fix, few clicks and done.

The second one made me hugely upset. They rejected my business model for the app saying that the features I have implemented are not sufficient to use auto-renewable subscriptions. They wanted me to downgrade to nonrenewable subscriptions. I talked with few people what they think about this and they didn’t also understand this statement. I asked for more explanations in the resolution centre. I waited a few hours for their answer and made one more mistake. I have uploaded second binary with minor fixes and pressed “Submit for Review” button. Just a second after this I realised that I was thrown away from getting an answer to my questions. I had to wait for the second review. That hours were so long … and the result was the same. Rejected

What violations now? Well, the first one was resolved, but with the second one, they said the same. I cannot agree with them and thought I will give myself one more chance to have it accepted my way.

My plan for launching on the 18th of June felt away. With WWDC 20 in a few days, I decided to work on it for a few weeks more and postpone the launch until half of July.

Hidden gem

I had one feature on my Todo list which I planned for a future version of Code Conf app. But after some consideration, I felt this could be the game-changer to Apple Review. It was dynamic, it was using one of fast-evolving Apple framework. This feature was “Recommendations”. I used for them Machine Learning and MLRecommender model from Core ML framework. I have prepared a rating matrix for more advanced and precise recommendations. This required also updates to API so I was able to retrieve training data for my model. With a dummy test data, the training worked without issues. But when I tried to use real data generated by my Testflight testers it started to break. I was looking for a solution almost everywhere. I even was considering dropping support for training with ratings and go the easier way. Hopefully, just after WWDC, I posted a tweet which interested one of Apple Engineers in CoreML team. I filed Feedback, posted on Developer Forums. And found the solution on StackOverflow. If you want to read more on this issue I wrote a post on it with more details.

While I resolved training issues I wanted to update model remotely so Code Conf users are well-trained model between new releases of my app. It was possible because Apple allows you to recompile downloaded model on the device and use this recompiled version same way as I was embedded in the binary. Everything worked fine.

In between, I have implemented also Image caching for better user experience based on awesome post by Vadim Bulavin. Having my gem implemented and resolved a few issues found in the meantime, it was time for last chance with Apple Review.

Let’s launch it one more time

I was ready for review. But I was scared with its probable result. I was worried if this time I will get rejection one more time?

On Friday evening I got one more chat with Donny (I swear this is the last time I quote him here ;) ) about my struggles. He replied to me “uh oh :o I’d say go for it!” and assured that I will get easy weekend as Apple Reviewers do not work on weekends.

And you know what? On Saturday evening (CEST timezone) the status of my app changed to “In Review”. How I was stressed then. Fortunately, I went to sleep and slept until 4 AM when I got notification from App Store Connect App. “You got a new message in resolution centre”. I woke up in a second. Whaaat? Why? Not again …

When I logged to read it I saw “Metadata Rejected”. And I calmed down. I knew that my binary with auto-renewable subscriptions is accepted. The only thing was missing 3 lines about those subscriptions in my app description. I have updated it and replied for reviewing metadata again. In about half an hour I got my app APPROVED FOR SALE and “Pending developer action” on Sunday morning. How happy I was with it.

Code Conf content

In between coding I was contacting conference organizers for permission to use their talks in my app. I was pleased by such positive responses. As a result Code Conf app has a right to stream all content it contains without any legal issues. All talks are available to browse and watch for free.

Wrap up

This project took me 9 months to write. This is not the end of it but it just starts. I have many features yet to implement which will make it even better for you. I am working with next conference organisers to add their talks to Code Conf app. If you are a conference organiser and you would like to add your talks to my app please contact me on Twitter. DM’s are open. I want this app to be as much inclusive as possible. That’s why I would love to include talks given not only in English but also in other languages.

At this point, I wanted to say thank you to my beloved Wife for her patience while I was coding a lot. All my beta testers: Donny Wals, Dominic Williams, Ben Noland, Vadim Bulavin, Tibor Bodecs, Mateusz Matrejek, Artur Rymarz, Benedikt Tertechte, Niv Ben-Porath, Christian Weinberger, iMike for all their reports, suggestions and time.

My developer journey and success of this project probably won’t be possible without having access to great resources from the Swift community so additional thanks go to: Antoine van der Lee, John Sundell, Paul Hudson, Mark Moeykens, Majid Jabrayilov and many more I have learned from during this time.

Code Conf app is available in App Store all around the world from today - 14th of July 2020.

Download it now

Post scriptum

Sometimes you have to stay calm, step back and don't give up and push forward your dreams. My dream has become a reality today.
#App, #Code Conf, #Vapor, #SwiftUI, #API, #Backend

MLRecommender in practice

I was researching how to make personalized recommendations for my app “Code Conf”. As I have found I can use Machine Learning for this purpose and use CoreML MLRecommender model.

To train this model you need 2-3 inputs and as an output you gain set of recommendations based on input data.

It looks very easy and it is so if you know what I know now after almost 3 weeks of research.

Training MLRecommender model can be done in two ways:

  • using only reference between users and items for recommendations
  • using not only above but also user rating for items

My data set dumped from database looks like that:

user_id                               item_id                               score
f69df0fd-3b7e-489d-9197-28d94be3d281, 53fb60b1-7d6c-473f-91bf-42fd670ae055, 6
d1d1dad9-af15-4bc6-9066-5bc39a830eb0, 10034e91-1698-4f16-9cc2-483aa2e84372, 1
d1d1dad9-af15-4bc6-9066-5bc39a830eb0, 396a1d47-ca8f-4189-8526-85e40875c363, 35
8730655f-b7b9-4d36-a4c2-f48e866e4533, 53fb60b1-7d6c-473f-91bf-42fd670ae055, 1
d1d1dad9-af15-4bc6-9066-5bc39a830eb0, 4827fb22-4bd1-47f8-aee4-fb45b8900cb6, 1
d1d1dad9-af15-4bc6-9066-5bc39a830eb0, be9d114d-2a16-4e24-b142-44fc97351cc6, 1
d1d1dad9-af15-4bc6-9066-5bc39a830eb0, d8ab8d67-7efa-4370-b7d0-6d176c81901f, 1
9155b83d-d443-46d8-a24d-cff329eb0d07, 73bfd56b-b799-43cd-b17b-4ef259d18fcc, 35
d1d1dad9-af15-4bc6-9066-5bc39a830eb0, 53fb60b1-7d6c-473f-91bf-42fd670ae055, 1
d1d1dad9-af15-4bc6-9066-5bc39a830eb0, 22ca0dc8-1607-4f48-bef3-84a267607cf5, 1

So I have here table of user_ids and item_ids which are both UUID type and score which is Int but can also be Double.

As you look closer on this data you will see that some users rated few and some users rated more items. And on the other hand that some items are rated only by one user and others by more users.

It looks that this example data should be sufficient to train your model. But as I have learned it is not. Every time I wanted to learn my model I got this error:

Training Error: Item IDs in the recommender model must be numbered 0, 1, ..., num_items - 1

Very informative error which makes you do not know anything what is happening here.

Item IDs has to be numbered? Hey … but they are allowed to be String type.

It turned out at first that MLRecommender model wants you to have as input normalised data. What does it mean?

It means that it requires you to provide data where every item is rated by every user. You can ask: But how to force every user to rate every item to make this dataset normalised?

This is rather not possible at all. We have to go another way. My example data contains small number of users and items so it is possible to create such set of data by hand. It is not very productive but for testing purposes possible. What if this data set contains hundreds of users and thousands of items? Good luck to make it manually.

Fortunatelly there is a tool which can transform your data from CSV file and update it to suite MLRecommender requirements.

This tool is named pandas and it is a Python module to transform data sets. I have very brief idea how it works so I won’t exaplain it here deeply but I will share with you my script which works for me later on.

To install pandas use Python’s pip command:

$ pip install pandas

Here is my pandas script with comments what is going on. This script is probably not the most efficient one but I have zero experience with writing python code so when it returned valid dataset I leaved it as is.

import csv
import numpy
import pandas as pd
import uuid

# 1. Read input CSV file
ratings = 'x7.csv'
ratings_df = pd.read_csv(ratings)

# 2. get unique user_id's and item_id's
item_ids = ratings_df.item_id.unique()
user_ids = ratings_df.user_id.unique()

# 3. make temporary ratings dataset
new_ratings_df = ratings_df

# 4. iterate by items and add all missing user ratings
for item_id in item_ids:
    mock = pd.DataFrame({'item_id': item_id, 'user_id': user_ids, 'score': 0.000001})
    new_ratings_df = new_ratings_df.append(mock)

# 5. iterate by users and add all missing items
for user_id in user_ids:
    mock = pd.DataFrame({'item_id': item_ids, 'user_id': user_id, 'score': 0.000001})
    new_ratings_df = new_ratings_df.append(mock)

# 6. drop all duplicates and leave only first values (the original ratings + new added where they were missing)
new_ratings_df.drop_duplicates(subset=['user_id', 'item_id'], keep='first', inplace=True)

# 7. sort by items column
new_ratings_df = new_ratings_df.sort_values(by=['item_id'], ignore_index=True)

# 8. export data to new CSV file
new_ratings_df.to_csv('new_ratings.csv', quoting=csv.QUOTE_NONNUMERIC, index=False)

You probably have noticed score I have added to this dummy data. In my use case I am working with ratings 1 - 40. This dummy rating cannot interfere with my real user ratings. This is why its 0.000001.

Be warned here: You cannot use as ratings values = 0. The rating has to be greater than zero to meet MLRecommender requirements. If this would be the only one thing I wanted you to remember from this article this is it.

Now when we run pandas script on original data set it gives us such working data:

"user_id"                             , "talk_id"                             , "score"
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "10034e91-1698-4f16-9cc2-483aa2e84372", 1.0
"9155b83d-d443-46d8-a24d-cff329eb0d07", "10034e91-1698-4f16-9cc2-483aa2e84372", 1e-06
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "10034e91-1698-4f16-9cc2-483aa2e84372", 1e-06
"f69df0fd-3b7e-489d-9197-28d94be3d281", "10034e91-1698-4f16-9cc2-483aa2e84372", 1e-06
"9155b83d-d443-46d8-a24d-cff329eb0d07", "22ca0dc8-1607-4f48-bef3-84a267607cf5", 1e-06
"f69df0fd-3b7e-489d-9197-28d94be3d281", "22ca0dc8-1607-4f48-bef3-84a267607cf5", 1e-06
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "22ca0dc8-1607-4f48-bef3-84a267607cf5", 1e-06
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "22ca0dc8-1607-4f48-bef3-84a267607cf5", 1.0
"9155b83d-d443-46d8-a24d-cff329eb0d07", "396a1d47-ca8f-4189-8526-85e40875c363", 1e-06
"f69df0fd-3b7e-489d-9197-28d94be3d281", "396a1d47-ca8f-4189-8526-85e40875c363", 1e-06
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "396a1d47-ca8f-4189-8526-85e40875c363", 1e-06
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "396a1d47-ca8f-4189-8526-85e40875c363", 35.0
"9155b83d-d443-46d8-a24d-cff329eb0d07", "4827fb22-4bd1-47f8-aee4-fb45b8900cb6", 1e-06
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "4827fb22-4bd1-47f8-aee4-fb45b8900cb6", 1.0
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "4827fb22-4bd1-47f8-aee4-fb45b8900cb6", 1e-06
"f69df0fd-3b7e-489d-9197-28d94be3d281", "4827fb22-4bd1-47f8-aee4-fb45b8900cb6", 1e-06
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "53fb60b1-7d6c-473f-91bf-42fd670ae055", 1.0
"9155b83d-d443-46d8-a24d-cff329eb0d07", "53fb60b1-7d6c-473f-91bf-42fd670ae055", 1e-06
"f69df0fd-3b7e-489d-9197-28d94be3d281", "53fb60b1-7d6c-473f-91bf-42fd670ae055", 6.0
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "53fb60b1-7d6c-473f-91bf-42fd670ae055", 1.0
"f69df0fd-3b7e-489d-9197-28d94be3d281", "73bfd56b-b799-43cd-b17b-4ef259d18fcc", 1e-06
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "73bfd56b-b799-43cd-b17b-4ef259d18fcc", 1e-06
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "73bfd56b-b799-43cd-b17b-4ef259d18fcc", 1e-06
"9155b83d-d443-46d8-a24d-cff329eb0d07", "73bfd56b-b799-43cd-b17b-4ef259d18fcc", 35.0
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "be9d114d-2a16-4e24-b142-44fc97351cc6", 1.0
"f69df0fd-3b7e-489d-9197-28d94be3d281", "be9d114d-2a16-4e24-b142-44fc97351cc6", 1e-06
"9155b83d-d443-46d8-a24d-cff329eb0d07", "be9d114d-2a16-4e24-b142-44fc97351cc6", 1e-06
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "be9d114d-2a16-4e24-b142-44fc97351cc6", 1e-06
"d1d1dad9-af15-4bc6-9066-5bc39a830eb0", "d8ab8d67-7efa-4370-b7d0-6d176c81901f", 1.0
"f69df0fd-3b7e-489d-9197-28d94be3d281", "d8ab8d67-7efa-4370-b7d0-6d176c81901f", 1e-06
"8730655f-b7b9-4d36-a4c2-f48e866e4533", "d8ab8d67-7efa-4370-b7d0-6d176c81901f", 1e-06
"9155b83d-d443-46d8-a24d-cff329eb0d07", "d8ab8d67-7efa-4370-b7d0-6d176c81901f", 1e-06

Having this file exported we can learn our MLRecommender model using new in Xcode 11 CreateML app.

Create new project, choose Recommender model type and then use our new normalised data set to train this model. After it is trained you can use copy it to your app and start using.

Here I wanted to say thank you to the person on StackOverflow who was also bothering with same error and who provided me to find a solution for it. It’s mpmontanez and his topic on StackOverflow: https://stackoverflow.com/questions/62270353

I also wanted to say thanks Apple Engineers on forums and Twitter who got interested in my case. They have not provided me above solution but I know that this issue will probably be fixed in new macOS Big Sur.

Other useful resources:

My feedback number for this issue is: FB7854032 & Apple Developers Forum Topic

If you have any questions or something is not clear enough about my MLRecommender journey feel free to ask me on Twitter.

  • CSV data has added spaces after commas for better article readability.
#CoreML, #MLRecommender, #Machine Learning

Multi Select Picker for SwiftUI

I was looking for ability to select multiple items. During my exploration I have found that there is no possiblity to make it select more than one item. This fact was also confirmed by Paul Hudson on his #Slack channel. Being in this situation I searched for any examples of such code.

This led me to Stackoverflow where I have found partially working solution. So after many refinements and tweaks I managed to simulate Picker and be able to select multiple items.

Below is complete working code with example Language Enum as source of choices. When you select “Choose languages” selection sheet is presented. When you select 0 or more item and hit “OK” button modal is dismissed and counter on “Choose languages” row is updated to number of items choosen.

This code is very rough, and there are many places where it could be written better, but this is its first implementation which works as I wanted.

If you have any comments about below elements feel free to comment on Twitter.

This is main view which will present Picker:

ContentView

import Combine
import SwiftUI

struct ContentView: View {
    @State private var showLanguageSheet = false

    @State private var x = 0

    @ObservedObject var preferedLanguages = PreferedLanguages()

    var body: some View {
        NavigationView {
            VStack {
                Form {
                    Section(header: Text("Language").font(.caption)) {
                        Button(action: {
                            self.showLanguageSheet.toggle()
                        }) {
                            HStack {
                                Text("Choose languages").foregroundColor(Color.black)
                                Spacer()
                                Text("\(preferedLanguages.languages.count)")
                                    .foregroundColor(Color(UIColor.systemGray))
                                    .font(.body)
                                Image(systemName: "chevron.right")
                                    .foregroundColor(Color(UIColor.systemGray4))
                                    .font(Font.body.weight(.medium))

                            }
                        }
                        .sheet(isPresented: $showLanguageSheet) {
                            SettingsLanguagePickerView(self.preferedLanguages)
                        }

                        Picker(selection: $x, label: Text("One item Picker")) {
                           ForEach(0..<10) { x in
                              Text("\(x)")
                           }
                        }

                        NavigationLink(destination: self) {
                            Text("Default navigation link")
                        }
                    }
                }
            }
            .navigationBarTitle("Content")
        }
    }
}

Source of Picker items - in this example Language enum

Language Enum

import Combine
import SwiftUI

enum Language: Int, CaseIterable, Identifiable {
    case english = 0
    case polish

    var id: Language {
        self
    }

    var literal: String {
        switch self {
        case .english: return "English"
        case .polish: return "Polish"
        }
    }
}

Combine helper which passes updated values between views and updates picker with previously choosen values

Combine Helper class to expose selected language array

class PreferedLanguages: ObservableObject {
    @Published var languages = [Language]()

}

Main Picker View

Multiple Select Picker

struct SettingsLanguagePickerView: View {
    @State private var selections = [Language]()

    @ObservedObject var preferedLanguages: PreferedLanguages

    init(_ preferedLanguages: PreferedLanguages) {
        self.preferedLanguages = preferedLanguages
    }

    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        NavigationView {
            List {
                Section(header: Text("Choose prefered languages")) {
                    ForEach(Language.allCases) { item in
                        MultipleSelectionRow(title: item.literal, isSelected: self.selections.contains(item)) {
                            if self.selections.contains(item) {
                                self.selections.removeAll(where: { $0 == item })
                            }
                            else {
                                self.selections.append(item)
                            }
                        }
                    }

                }
            }
            .onAppear(perform: { self.selections = self.preferedLanguages.languages })
            .listStyle(GroupedListStyle())
            .navigationBarTitle("Languages", displayMode: .inline)
            .navigationBarItems(trailing:
                Button(action: {
                    self.preferedLanguages.languages = self.selections
                    self.presentationMode.wrappedValue.dismiss()
                }) {
                    Text("OK")
                }
            )
        }
    }
}

Picker Row

Multiple select row

struct MultipleSelectionRow: View {
    var title: String
    var isSelected: Bool
    var action: () -> Void

    var body: some View {
        Button(action: self.action) {
            HStack {
                Text(self.title)
                if self.isSelected {
                    Spacer()
                    Image(systemName: "checkmark").foregroundColor(.blue)
                }
            }
        }.foregroundColor(Color.black)
    }
}

Credits for base implementation of Multiple Select Picker goes to graycampbell and his implementation which I have found at StackOverflow: https://stackoverflow.com/a/57023746/1285959.

This article was mentioned in:

#SwiftUI, #Picker