Accessibility iOS Localization SwiftUI · 4 min read
Pluralized Phrases Localization
Localizing plural sentences can be challenging. Let's learn what Apple gives us to tackle this specific problem.
I was working on an upcoming app called Charabia , and I needed some ways to log the user into the app. But I wanted that experience to be really smooth; I don't want to pop up a signup or login form. So I thought that Sign-in with Apple would be a good fit for that, so I added it plus the option to sign in with Google. The logic is pretty simple. Once you tap on one of these buttons, you authenticate via the client SDK. For Sign in with Apple (or SIWA, I like to call it đ), it's the AuthenticationServices framework that takes care of that, and for Google, I've installed the Google Sign-In Swift package. Once authenticated, both SDK will send you a token or authorization code you can send to your backend server for verification. That's the tricky part because it requires a lot of configuration beforehand in order to get all the different elements necessary for the validation. After a successful verification, depending on the user's existence in the database, we might create the user from the decoded pieces of information contained in the JWT token. If he doesn't exist yet, we create the record in the user table and send him back to the client with an access token he can use for subsequent requests on the RESTful API. Let's see how to do all that in this article. I've separated this guide into three main sections:
Excited? Let jump right into it without any further ado.
Let's first start by creating the app. It's a single app that uses SwiftUI.
For the sake of simplicity, we'll use a single file, the ContentView, to host all of our code (and it shouldn't be long đ
). iOS 14 added a convenient way to sign in with Apple with a view named
SignInWithAppleButton
that is directly configurable via its initializer.
SignInWithAppleButton(
label: SignInWithAppleButton.Label,
onRequest: (ASAuthorizationAppleIDRequest) -> Void,
onCompletion: ((Result<ASAuthorization, Error>) -> Void)
)
The onRequest
and onCompletion
arguments are both closures, the first to configure the request and the second to process the result. I'll put these methods in a ViewModel to decouple the view from the logic. It looks like this:
import SwiftUI
import Combine
import AuthenticationServices
final class ViewModel: ObservableObject {
func handle(request: ASAuthorizationAppleIDRequest) {
}
func handle(completion result: Result<ASAuthorization, Error>) {
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Spacer()
Text("đ Hello SIWA")
.font(.title)
Spacer()
SignInWithAppleButton(
.continue,
onRequest: viewModel.handle(request:),
onCompletion: viewModel.handle(completion:)
)
.frame(height: 48)
}.padding()
}
}
For the authorization request, let ask for user email
and the fullName
.
func handle(request: ASAuthorizationAppleIDRequest) {
request.requestedScopes = [.fullName, .email]
}
Go to the project settings and select your main target; in "Signing & Capabilities" tab, add the "Sign in with Apple" capability.
Now you can run the app. Note that you have to use a physical device to test SIWA; there is an unresolved bug since iOS13 simulators.
Anyway, it should look like this.
Once the button is tapped, you'll have the SIWA popup and you can either use your email or a private one that'll forward to your real email and set a name as well.
Be aware; this popup will appear only once before you accept it via Face-Id or Touch-Id; for subsequent login, you'll have another popup for authorization. Thus, if you want the user pieces of information like his name, you have to handle it the first time the popup is shown. But if you messed up, I will show you how you can later reset the popup and show it like it was the first time.
Now, once we accept (or deny), the handle completion method is fired, and we can send the authorization code provided to our backend for validation.
final class ViewModel: ObservableObject {
func handle(request: ASAuthorizationAppleIDRequest) {
request.requestedScopes = [.fullName, .email]
}
func handle(completion result: Result<ASAuthorization, Error>) {
switch result {
case .success(let authorization):
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
let tokenData = credential.authorizationCode,
let token = String(data: tokenData, encoding: .utf8)
else { print("error"); return }
send(token: token)
case .failure(let error):
print(error.localizedDescription)
}
}
private func send(token: String) {
guard let authData = try? JSONEncoder().encode(["token": token]) else {
return
}
let url = URL(string: "https://yourbackend.example.com/tokensignin")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in
// Handle response from your backend.
}
task.resume()
}
}
That's all we need to do for our client. That's pretty simple, right. Now let's handle the public and secret keys configuration that'll help us validate the token generated by the client.
Now let's create the keys (public and secret) we need to verify the client's authorization code.
siwatut
AuthKey_keyid.p8
to key.txt
key.txt
sudo gem install jwt
client_secret.rb
to process the private key and open it in your favorite text editorrequire 'jwt'
key_file = 'key.txt'
team_id = 'your-team-id'
client_id = 'dev.ibrahima.siwa-tut'
key_id = 'your-key-id'
ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file
headers = {
'kid' => key_id
}
claims = {
'iss' => team_id,
'iat' => Time.now.to_i,
'exp' => Time.now.to_i + 86400*180,
'aud' => 'https://appleid.apple.com',
'sub' => client_id,
}
token = JWT.encode claims, ecdsa_key, 'ES256', headers
puts token
team_id
can be found on the top-right corner when logged into your Apple Developer accountclient_id
is our appâs bundle identifierkey_id
is the private key identifier created at step 9 aboveruby client_secret.rb
If everything goes well, it should print a JWT token on the terminal.
Copy and save it somewhere in the meantime, and we will use it in our backend.
Now that we have generated our JWT token, let's process it in our backend to extract the pieces of information we need.
For the backend, Iâm going to use Laravel PHP framework. You can, of course, use any server-side framework (Express, Django, Rails, etc.) of your choice. I already have a fresh installation of Laravel.
Now I'll copy the token we generate earlier in the .env
, an environment file like so:
SIWA_CLIENT_ID=dev.ibrahima.siwa-tut # your xcode bundle identifer used to generate the token
SIWA_CLIENT_SECRET= # the jwt token generated earlier
SIWA_GRANT_TYPE=authorization_code # used in the validation request
Now let's add an endpoint that the client'll hit. I'll go to the web.php route file, and I create a controller for handling the request.
// web.php
Route::post('tokensignin', [AuthController::class, 'handleSIWALogin']);
// AuthController.php
<?php
namespace App\Http\Controllers;
use App\Models\User;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
use GuzzleHttp\Exception\GuzzleException;
class AuthController extends Controller
{
public function handleSIWALogin()
{
$authorizationCode = request()->input('token'); // 1
$body = 'client_id=' . env('SIWA_CLIENT_ID') . '&client_secret=' . env('SIWA_CLIENT_SECRET') . '&code=' . $authorizationCode . '&grant_type=authorization_code';
$client = new Client();
$request = new Request("POST", "https://appleid.apple.com/auth/token", ["Content-Type" => "application/x-www-form-urlencoded"], $body); // 2
try {
$response = $client->send($request); // 3
$data = json_decode($response->getBody(), true);
$payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', explode('.', $data['id_token'])[1]))), true); // 4
if ($payload['email']) return $this->createOrLogUser($payload); // 5
return $this->respondWithError("Could not authenticate with this token");
} catch (GuzzleException $e) {
return $this->respondWithError($e->getMessage()); // 6
}
}
private function createOrLogUser($payload): JsonResponse
{
// create the user if he's not yet in the database or return him with (if he already exists) with a token and send it back to the client
}
private function respondWithError($message): JsonResponse
{
// return a json response representing an error
}
}
Don't be intimidated by the code above, and it's pretty straightforward. Let's see the different steps needed to verify the token:
json_decode
method in the id_token
fieldAs I told you before, the authorization popup where you give your name and decide to share or use a private email will show once. After accepting, there will be another popup for subsequent authorization. I know that you might want to retrieve all the user data like his name for testing, but if you haven't done that the first time, you have no chance to do it later. You can reset the Sign in with Apple authorization popup by following these steps.
As you can see, validating the authorization code provided by Sign in with Apple isnât difficult, it just requires some setup that can be tedious, I know đ. For more informations, I recommend reading the docs about user verification and token validation from Apple docs:
Written by
Independant developer, iOS engineer since 2014. I write articles about Swift, SwiftUI, iOS accessibility, software design, app architecture, testing and more.
New posts about Swift, SwiftUI, TDD, software design, accessibility, testing in iOS directly in your inbox. I won't spam ya. đ Unsubscribe anytime.