Vladimir Kelin - Development
Blog about iOS development. Code snippets, best practice, discussions, stories, jokes and tutorials - everything that I can't post on Stackoverflow.
Monday, March 11, 2019
How to Record Calls on iPhone
Here is another way involving a Mac and EarPods.
Attach EarPods to the Mac. Then attach the remote controller of the EarPods (which contains a microphone) to the iPhone. The microphone should be attached near the speaker. To fix it on the phone you can use a tape or a bulldog clip. Or just clamp it under the cover:
Use QuickTime to create a sound file. Click File > New Audio Recording. Since you attached your headphones to the Mac, the External microphone will be selected by default.
Then hit the red button to start the recording. Make a call.
The described technique doesn't require speaker mode and gives a good sound quality without noises. Your mouth is farther from the EarPods micro than the speaker of the iPhone. The distance compensates the differences in the sound volume, thus the both voices in the call will have similar loudness.
Tuesday, February 19, 2019
Where Smart Watches Gone Wrong
Could it be better?
It would be, if exterior and interior of smartwatches was produced by different companies. The idea is to specialize. The hulls and bands could be made by a classic watch manufacturers and fashion houses. Аnd the smart parts, like hardware and software should be made IT companies, like Apple, Samsung, Xiaomi or Microsoft. Well-known watch brands like Louis Vuitton, Rolex or Omega produce luxury goods, they haven't enough resources and expertise to create their own hardware design or operating system. IT giants, in turn, being oriented on broad market and unified products, couldn't focus on individual design and bring such huge and artistic variety of looks. Even in "gold edition".Benefits
What would we, the customers, get from such change in the industry?- Nicer watches.
- Higher flexibility.
- Lower price of the update (if the old body used with new electrical parts. The watch body can even go from father to son!)
Examples
What will happen if watches turn the correct way? Some examples:- A G-Shock-like Apple Watches. A sports watch in robust shock resistant shell. Awesome? Awesome.
- Beautiful and expensive watches like Blancpain. With all new updates.
- Retrofitting. You'll be able to update an old grandfarther's watch with a new powerful hardware.
Sunday, October 28, 2018
How to get old WWDC videos
It appears that old WWDC videos are removed from the official list. This post tells how to get them.
So, if you visit the developers's site from the above link you will see that currently available year range is only 2015–2018: Even the search yields no results: I had such a problem trying to find 8 year old video (WWDC 2010, Core Animation in Practice). Thanks to reddit community help, several solutions were found:
So, if you visit the developers's site from the above link you will see that currently available year range is only 2015–2018: Even the search yields no results: I had such a problem trying to find 8 year old video (WWDC 2010, Core Animation in Practice). Thanks to reddit community help, several solutions were found:
- WWDC videos prior to 2012 can be found in Developer Video Archive.
- From 2012 to 2015 they can be accessed by the link: https://developer.apple.com/videos/wwdc2012/. Just change the year in the end.
- The keynotes can be found in Podcasts.
Thursday, October 18, 2018
UIKit protocol of the month: StoryboardInstantiatableViewController
Github Gist.
I want to share this little invention with the community. I use it for the last month and find it notably helpful:
This protocol along with the extension provides some syntactic sugar and solves two common problems:
I want to share this little invention with the community. I use it for the last month and find it notably helpful:
protocol StoryboardInstantiatableViewController {
static var storyboardId: String { get }
}
extension StoryboardInstantiatableViewController where Self: UIViewController {
static func instantiate(from storyboard: UIStoryboard?) -> Self? {
return storyboard?.instantiateViewController(withIdentifier: Self.storyboardId) as? Self
}
}
This protocol along with the extension provides some syntactic sugar and solves two common problems:
- Shortens the instantiation
- Answers, where to store the storyboard id
Usage example:
// Declaration:
class TaskViewController: UIViewController, StoryboardInstantiatableViewController {
// The same id as specified in Identity inspector
static let storyboardId: String = "task"
// ...
}
// Initializing from Storyboard:
// ...
guard let taskVC = TaskViewController.instantiate(from: storyboard) else { return }
navigationController?.pushViewController(taskVC, animated: true)
Update:
Surprisingly, I bumped into another similar protocol, namedStoryboardInstantiatable
. The main difference that it's designed for use with NSViewController
and Cocoa instead of Cocoa Touch. The fact is, that the gist was posted 5 months earlier than this blog post. So Satori Maru is the first one who invented the idea, although I came to it independently.
Wednesday, August 22, 2018
Errors in WWDC 2018
Some errors in the translation example. WWDC 2018, Session 404: New Localization Workflows in Xcode 10, slide 34. Looks like additional words were added just to make EN->RU translation look more complicated :)
Tuesday, August 14, 2018
Sorting and Choosing Keywords for App Store
Updated: 12 Mar 2019.
Suppose you already have plenty keyword ideas for your app. Or, at least, you understand how to select them.
There are so many promising keywords, but in App Store their total length is limited to 100 bytes including commas. This post describes a sensible way to select a few keywords which will give the best search performance.
Your keyword ideas are:
You think the app page with such keywords will easily match searches like:
Now do the first simple yet important step: create a spreadsheet. Make a column named
For example, the request:
Create a column:
Other recommendations: don't specify any particular location to enlarge input data (if your app isn't country-bound), but specify the language. Best time range is "next month".
Now set English language and write Impressions to the spreadsheet under
But in the educational purpose you will determine the linguistic frequency of each keyword using Ngram (so many great tools by Google!). Paste first 12 comma-separated words in the search field, check "case-insensitive", select the 3 last years (currently the freshest data are from 2008, so select 2005-2008 range) and hit the Enter. Now you can take a screenshot of the values: and after spending some time while putting them into the
If we select all words with low competition, we will obtain the following:
Now sort the table by Concurrency, increasing. What do you see there? Some quite unpopular, low-concurrent yet meaningful in the context of your app words, like:
Another interesting group is so-called red words which are popular, highly concurrent but closely describe your app. Such as
So, suppose, you drop
Plurals can be replaced with singulars. The search algorithm will treat them the same. This also true for verb vs. noun forms and for typos. So, you can replace
Choose not only keywords, but their combinations. For example
BTW, you probably already noticed that
In conclusion, we've got 14 most efficient keywords of total length 97 characters without whitespaces:
The example spreadsheet is available in HTML and Google Sheets.
Suppose you already have plenty keyword ideas for your app. Or, at least, you understand how to select them.
There are so many promising keywords, but in App Store their total length is limited to 100 bytes including commas. This post describes a sensible way to select a few keywords which will give the best search performance.
Organize Keywords in a Spreadsheet
Additionally, suppose that the target app is named Plant Classifier and it's a visual recognition deep machine learning neural network startup based app, allowing users to identify a plant by it's photo. Here's its icon: (it's the kenaf and not what you think)Your keyword ideas are:
taxonomy, systematics, species, genus, name, identifier, identify, determine, search, herbarium, botany, nomenclature, photo, picture, image, atlas, visual, recognition, analysis, flower, tree, shrub, weed, herb, garden, leaf
. Their total length without whitespaces will be 200. It's actually a good practice to have at least twice as more words than you need, before you started to cull them.You think the app page with such keywords will easily match searches like:
plant species
identify tree genus
- or
determine flower by picture
Now do the first simple yet important step: create a spreadsheet. Make a column named
Keywords
and paste all 26 ideas there.
The spreadsheet will allow you to store keywords data in the convenient way and finally sort it. I prefer Google Sheets but any other tool will meet our demands.
Analyse App Store Search Results
The next thing you do is literally "go to App Store and search apps using each of the keyword candidates". Sure, this is the most precise way to evaluate the concurrency. The most competitive words will evoke more search results. Luckily we don't need to do this manually, because we have Keywording utility based on iTunes Search API.For example, the request:
./keywording "taxonomy,species" NZ
will give:
NZ
species 164
taxonomy 26
Create a column:
Concurrency, App Store results
. Set the country to more popular US store and perform search for all the words, filling the newly created column:
Estimate Search Volume
To estimate the search volume we will use Google Ads Keyword Planner. It can tell, how many searches users perform in "Google and search partners" network, and again, it's not the data we desire, but we assume, that Google searches will be quite close to the App Store ones. After you open the Planner, follow this steps:- Select "Get search volume and forecasts" box.
- Paste keywords there, and click GET STARTED.
- You will see the results under FORECAST section.
photo
, picture
and image
have enormously heightened clicks.Other recommendations: don't specify any particular location to enlarge input data (if your app isn't country-bound), but specify the language. Best time range is "next month".
Now set English language and write Impressions to the spreadsheet under
Impressions, Google
column. You will get something like this:
Determine Linguistic Frequency
Linguistic frequency is especially helpful with languages you are not very good at. If you found a couple of synonyms for a keyword, it can help you to find which one is more common to native speakers. Also, the frequency may help you to make a decision when the other indicators are equal. Generally, this step is optional and can be skipped.But in the educational purpose you will determine the linguistic frequency of each keyword using Ngram (so many great tools by Google!). Paste first 12 comma-separated words in the search field, check "case-insensitive", select the 3 last years (currently the freshest data are from 2008, so select 2005-2008 range) and hit the Enter. Now you can take a screenshot of the values: and after spending some time while putting them into the
Ngram, frequency
column of the table, you will get:
Sorting and Choosing
Time for the final part for which we were preparing since the beginning of the article. Analyse your data. First, look at the Concurrency column. Most of the keywords have values < 200 and that means that the competition is sufficiently low. You likely to choose some of them. Now select all the data and sort it by Impressions, decreasing. Have a look at it: Noticed the words with low concurrency but high search volume? They are "golden", you choose them, definitely. First five are highlighted with yellow:garden, tree, leaf, name, herb
. Their total length is only 26. You need sufficiently more.If we select all words with low competition, we will obtain the following:
garden, tree, leaf, name, herb, analysis, shrub, recognition, herbarium, botany, visual, taxonomy, genus, species, identify, nomenclature, determine, systematics
(total length - 144). Some words have to be dropped.Now sort the table by Concurrency, increasing. What do you see there? Some quite unpopular, low-concurrent yet meaningful in the context of your app words, like:
herbarium, taxonomy, botany, shrub
, they are silver words (blue). If you need to drop some words, you better leave them.Another interesting group is so-called red words which are popular, highly concurrent but closely describe your app. Such as
flower, image, picture, photo
. Consider them if you need to add some words to the final selection.So, suppose, you drop
analysis, nomenclature, systematics, determine
(not very suitable, anyway). Now we you have exactly 100 characters: garden, tree, leaf, name, herb, shrub, recognition, herbarium, botany, visual, taxonomy, genus, species, identify
. So neat!
Other Considerations
But could it be neater? There is still a place for the art in the keyword optimisation.Plurals can be replaced with singulars. The search algorithm will treat them the same. This also true for verb vs. noun forms and for typos. So, you can replace
recognition
with recognize
saving 2 characters. This information isn't robust, in practice it works for English keywords, but keep in mind that Apple may change their algorithms any time.Choose not only keywords, but their combinations. For example
garden
keyword is very sweet according to our research. But, how folks supposed to use it? Make some lame request like app for garden
? You shouldn't rely on that. Instead, take at least one of "red" image-related words, let's say ... image
.BTW, you probably already noticed that
weed
is somehow super-popular in the App Store, but has 0 impressions according to Google. It's the worst possible indicator. How could it be? If we open App Store and search for this keyword, we will see that store is literally spammed with trashy apps, which are unrelated to our topic. You better avoid such keywords if you want your app to be age rated 4+.In conclusion, we've got 14 most efficient keywords of total length 97 characters without whitespaces:
image, tree, leaf, name, herb, shrub, recognize, herbarium, botany, visual, taxonomy, genus, species, identify
. And most important, you learned predictable (almost scientific!) way to gauge keywords, to finally make a sensible selection which will elevate your app to the top of the targeted search results.The example spreadsheet is available in HTML and Google Sheets.
Thursday, July 19, 2018
Tuesday, July 10, 2018
Support Story: "Pls, Make a Tutorial"
As an individual developer, I convinced to fulfil support job for my customers. Each app in App Store should have a support link. For this purpose I use Google Forms (free hosting, pretty convenient). Periodic Timer also has a Facebook group. And that's amazing, that people really tend to write questions and suggestions to me! Though there always a percent of offtopic. I think the regular sharing of funny conversations is something that may happen on this blog.
And today's hero (call him Wob for anonymity) was someone, who pretty obstinately wanted me tutorial to the Periodic Timer (which is totally simple and intuitive app, BTW). He wrote directly to my support email and we had this funny conversation:
Wob <wobbbb@gmail.com>
Feb 14
to me
I just downloaded Periodic Timer but can find no instructions on how to use and fine-tune it.
Can I set the sound it makes or the volume, for example?
Do you have a set of instructions you can email me?
Thanks.
Periodic Timer Support <periodictimer.sup@gmail.com>
Feb 14
to Wob
Periodic Timer is simple and intuitive app that doesn't provide complicated options and can be can be understood without any instructions.
Change volume of Periodic Timer like you change like you do it with Alarm or Ringtone.
Good Luck :)
Wob <wobbbb@gmail.com>
Feb 14
to me
If it were as simple and intuitive as you claim, I wouldn’t have asked you for basic instructions. Please explain. (I can’t be the only person who’s asked for this information.)
Periodic Timer Support <periodictimer.sup@gmail.com>
Feb 14
to Wob
You are :)
How about the first link from the google? https://www.wikihow.com/Adjust-the-Volume-on-iOS-10
Have a nice day.
Wob <wobbbb@gmail.com>
Feb 15
to me
I think you misunderstand. I didn’t want just info about volume.
How do I use your app: 1, 2, 3? It’s too minimalist for me.
Thanks.
And then the story continues, tutorial-guy creates a new email for disguise, something like w.wob@gmail.com, and continues to solicit me for tutorial. I answered quite roughly:
Just look at yourself:
I can’t figure out how to successfully use your app. It’s very frustrating. Can you make a better tutorial? Also, I’ve already upgraded but i keep getting reminders to upgrade again. Please fix these bugs
You are creating multiple emails and spam me with "ples make a tutorial" messages. Why do you even do this? I suppose you are one of retired mans who has plenty of free time but no one to talk to.
I want to tell you one simple thing: this app is my hobby so, please, don't wait I will run around you trying to satisfy any of your whims. I have no free time, and also this app doesn't brings any money at all.
So, I hope you quite clever to understand me, if not - don't be surprised why I ignore you.
Still not sure, was that an attempt to flirt with me?
And today's hero (call him Wob for anonymity) was someone, who pretty obstinately wanted me tutorial to the Periodic Timer (which is totally simple and intuitive app, BTW). He wrote directly to my support email and we had this funny conversation:
Wob <wobbbb@gmail.com>
Feb 14
to me
I just downloaded Periodic Timer but can find no instructions on how to use and fine-tune it.
Can I set the sound it makes or the volume, for example?
Do you have a set of instructions you can email me?
Thanks.
Periodic Timer Support <periodictimer.sup@gmail.com>
Feb 14
to Wob
Periodic Timer is simple and intuitive app that doesn't provide complicated options and can be can be understood without any instructions.
Change volume of Periodic Timer like you change like you do it with Alarm or Ringtone.
Good Luck :)
Wob <wobbbb@gmail.com>
Feb 14
to me
If it were as simple and intuitive as you claim, I wouldn’t have asked you for basic instructions. Please explain. (I can’t be the only person who’s asked for this information.)
Periodic Timer Support <periodictimer.sup@gmail.com>
Feb 14
to Wob
You are :)
How about the first link from the google? https://www.wikihow.com/Adjust-the-Volume-on-iOS-10
Have a nice day.
Wob <wobbbb@gmail.com>
Feb 15
to me
I think you misunderstand. I didn’t want just info about volume.
How do I use your app: 1, 2, 3? It’s too minimalist for me.
Thanks.
And then the story continues, tutorial-guy creates a new email for disguise, something like w.wob@gmail.com, and continues to solicit me for tutorial. I answered quite roughly:
Just look at yourself:
I can’t figure out how to successfully use your app. It’s very frustrating. Can you make a better tutorial? Also, I’ve already upgraded but i keep getting reminders to upgrade again. Please fix these bugs
You are creating multiple emails and spam me with "ples make a tutorial" messages. Why do you even do this? I suppose you are one of retired mans who has plenty of free time but no one to talk to.
I want to tell you one simple thing: this app is my hobby so, please, don't wait I will run around you trying to satisfy any of your whims. I have no free time, and also this app doesn't brings any money at all.
So, I hope you quite clever to understand me, if not - don't be surprised why I ignore you.
Still not sure, was that an attempt to flirt with me?
Saturday, June 30, 2018
Handling background touches: UIControl instead of UITapGestureRecognizer
Usually, to handle background touches on a View Controller we add a
What you need to do is just to change the root view type from
If any child control interrupts a touch, it wouldn't be delivered to the background control! This is the main pro and con of this method. Sometimes it's exactly the desired behaviour to ignore touches on buttons. But other times you need to handle all the touches anywhere in the View Controller, like if you have a large UITextView and you want to hide the keyboard by tap.
UITapGestureRecognizer
. The setup requires a few lines of code, but there is a way, you can handle background touches even more easy!What you need to do is just to change the root view type from
UIView
to UIControl
and attach an action. With Storyboard:
-
Select the root view.
-
In the Identity inspector simply change the type to
UIControl
. (Serious type change isn't required.)
-
Finally, connect an action to the Touch Down event using Connections inspector.
@IBAction func backgroundTouched(_ sender: Any) {
// Hide keyboard
view.endEditing(true)
}
If any child control interrupts a touch, it wouldn't be delivered to the background control! This is the main pro and con of this method. Sometimes it's exactly the desired behaviour to ignore touches on buttons. But other times you need to handle all the touches anywhere in the View Controller, like if you have a large UITextView and you want to hide the keyboard by tap.
Monday, June 4, 2018
How to convert HTML to NSAttributedString?
Well, simply don't. Use WKWebView instead.
This will allow you to insert pictures, reuse CSS-styles, execute scripts etc.
You may find many third-party solutions on GitHub (DTCoreText and many others). They can be faster, then the built-in converter, but still slower comparing to WebKit. I tried some, and they all were buggy and most are poorly supported after iOS7 release brought those "initWithHTML" methods.
The only advantage of NSAttributedString is the ability to put it in UI elements like UILabel or UIButton.
This all may seem overcomplicated to you, but once you implement the logic, you will have a very useful tool.
This will allow you to insert pictures, reuse CSS-styles, execute scripts etc.
Wait, why not to use NSAttributedString?
While Apple provides several methods to initialize an instance of NSAttributedString from an HTML data, like:init(data:options:documentAttributes:)
, they are all extremely slow, about 1000 times slower than WKWebView which is as fast as Safari itself. Additionally, NSAttributedString supports a limited set of HTML attributes.You may find many third-party solutions on GitHub (DTCoreText and many others). They can be faster, then the built-in converter, but still slower comparing to WebKit. I tried some, and they all were buggy and most are poorly supported after iOS7 release brought those "initWithHTML" methods.
The only advantage of NSAttributedString is the ability to put it in UI elements like UILabel or UIButton.
WKWebView example
Here follows super-advanced example demonstrating widest possible abilities of displaying rich and interactive text with HTML, CSS, and JavaScript. To make things convenient and reusable the HTML document is split into parts:Tutorial_main.html
The main HTML that contains necessary elements that supposed to be involved in every page of the tutorial. Including:- A
<head>
block containing:- A raw CSS
<style>
section, containing easy-configurable constants. - An external CSS
"stylesheet"
. - The extremely important
"viewport"
<meta>
tag, required to scale content of a WKWebView correctly. - A
<script>
tag that provides Tutorial.js script. Not important enough to be listed here.
- A raw CSS
- A customisable
<body>
section.
<html>
<head>
<style type="text/css">
:root {
--softTextColor: %@;
--darkTextColor: %@;
--actionColor: %@;
--backgroundColor: %@;
--bodyFontSize: %@;
--titleFontSize: %@;
}
</style>
<link rel="stylesheet" type="text/css" href="Tutorial.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="Tutorial.js"></script>
</head>
<body>
%@
</body>
</html>
The %@
format specifiers will be replaced later with strings. Tutorial.css and Tutorial.js are contained in the Main Bundle root folder, so you don't need to specify baseURL
to the WKWebView. Yes, you can refer local files, even use anchor links.Tutorial.css
body {
color: var(--softTextColor);
font-family: "-apple-system";
font-size: var(--bodyFontSize);
margin-top: 0px;
text-align: left;
-webkit-user-select: none;
-webkit-touch-callout: none;
background-color: var(--backgroundColor);
}
h1 {
text-align: center;
font-size: var(--titleFontSize);
font-weight: 300;
color: var(--darkTextColor);
margin-top: 0.51em;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
}
a {
color: var(--actionColor);
text-decoration: none;
}
It's handy to keep CSS in a separate file. You can see some constants which already were listed before.step2_body.html
<h1>Сonnection</h1>
<p>Connect your device to your computer using the USB cable.</p>
<div class="spoiler">
<p>You can sync the device with iTunes using Wi-Fi.
In this case you will need the USB cable only once: to activate wireless syncing.
<a href="https://support.apple.com/en-us/HT203075">Read more.</a></p>
</div>
<div class="centered">
<a class="showMoreButton" href="showmore://">
<img src="show_more.svg" width=20 height=15 />show more
</a>
<a class="showLessButton" href="showless://">
<img src="show_less.svg" width=20 height=15 />show less
</a>
</div>
The body of the second step. Look at two buttons, there is something interesting about them. First, they use .svg images, because WKWebView supports vector graphics. (Images are also placed in the root directory of the Main bundle). Also, buttons use custom-scheme anchors, e.g.: href="showmore://"
. These anchors allows to intercept button clicks on the Swift-side using the webView(,decidePolicyFor:,decisionHandler:)
method of WKNavigationDelegate.Assemble
Time to put all the things together.func assembleHTMLString(bodyFileName: String) -> String? {
// All resources are contained in the Main Bundle
let bundle = Bundle.main
guard let mainHTMLURL = bundle.url(forResource: "Tutorial_main", withExtension: "html") else {
print("No main HTML!")
return nil
}
// The contents of Tutorial_main is not just a String, it's a format (see below)
guard let mainHTMLFormat = try? String(contentsOf: mainHTMLURL, encoding: .utf8) else {
print("Failed to parse \(mainHTMLURL.lastPathComponent)!")
return nil
}
guard let bodyURL = bundle.url(forResource: bodyFileName, withExtension: "html") else {
print("No body with name \(bodyFileName)!")
return nil
}
guard let htmlBody = try? String(contentsOf: bodyURL, encoding: .utf8) else {
debugLog("Failed to parse \(bodyFileName)!")
return nil
}
// Prepare some necessary arguments...
let arguments = [
// Following values will be used as CSS constants
UIColor.lightGray.hexString, // softTextColor
UIColor.gray.hexString, // darkTextColor
UIColor.green.hexString, // actionColor
UIColor.white.hexString, // backgroundColor
"15px", // bodyFontSize
"22px", // titleFontSize
htmlBody // the body, yes
]
// And here we ASSEMBLE! Finally.
let assembledHTMLString = withVaList(arguments) {
// The `arguments` substitute %@ in the `format`, one by one.
(NSString(format: mainHTMLFormat, arguments: $0)) as String
}
return assembledHTMLString
}
Wondering where is hexString
property of UIColor
coming from? – Read about the Power of Extensions.Configure WKWebView
You can configure it on your own. But if you don't know how to disable zooming, here is some help:webView.scrollView.delegate = self
extension TutorialViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return nil // Just return nil here
}
}
When you have the assembled HTML string, there is not much left to do:
webView.loadHTMLString(assembledHTMLString,
baseURL: Bundle.main.bundleURL)
This all may seem overcomplicated to you, but once you implement the logic, you will have a very useful tool.
Subscribe to:
Posts (Atom)