Pull to refresh
5
0.4
Алексей Сторожев @storoj

User

Британский сервис аренды Fetch доставляет электромобили с помощью дистанционного управления

мне в google stadia было очень дискомфортно в гонки играть из-за задержки, а тут настоящую машину по улице везти

Безопасная локализация строк в iOS: Localinter

Зачем в работающем приложении на горячую обновлять локализацию? Никто не умрёт, если новая локализация применится при следующем перезапуске. Зато сколько жизни можно сэкономить не поддерживая такую маргинальную фичу.

Использование Command Line Tool на Swift в iOS проекте

Итак, вот мой вариант:

import Foundation

let infoPlistURL = URL(fileURLWithPath: UserDefaults.standard.string(forKey: "infoPlist") ?? "Info.plist")

var format: PropertyListSerialization.PropertyListFormat = .xml
var plist = try! PropertyListSerialization
  .propertyList(from: try! Data(contentsOf: infoPlistURL),
                options: .mutableContainersAndLeaves,
                format: &format) as! NSMutableDictionary

let kvPairs = try JSONDecoder()
  .decode([DomainInfo].self, from: try readDomainsData())
  .map {
    ($0.name, [
      "NSExceptionAllowsInsecureHTTPLoads": $0.allowsInsecureHTTPLoads,
      "MyDescription" : $0.description
    ])
  }

plist["NSAppTransportSecurity"] = [
  "NSExceptionDomains": Dictionary(uniqueKeysWithValues: kvPairs)
]

try! PropertyListSerialization
  .data(fromPropertyList: plist, format: format, options: 0)
  .write(to: infoPlistURL)

struct DomainInfo: Decodable {
  let name: String
  let allowsInsecureHTTPLoads: Bool
  let description: String
}

func readDomainsData() throws -> Data {
  if let url = UserDefaults.standard.string(forKey: "from"), url != "-" {
    if url.hasPrefix("http://") || url.hasPrefix("https://") {
      return try Data(contentsOf: URL(string: url)!)
    }
    return try Data(contentsOf: URL(fileURLWithPath: url))
  }
  return FileHandle.standardInput.readDataToEndOfFile()
}
  • мы вроде согласились, что отдельный файл не нужен, поэтому обновляется только Info.plist;

  • Info.plist по дефолту ищется в текущей рабочей директории, или его можно передать аргументом -infoPlist;

  • json с данными может читаться из трёх источников:

    • updater -from https://.... – URL в интернете;

    • updater -from /local/path/on/disk – файл на диске;

    • updater -from - (dash) или же без аргументов – прочитается из stdin.

      • curl url | updater;

      • cat file | updater;

      • updater < file;

      • updater и ввод с завершающим Ctrd+D;

  • команда для удаления тоже не нужна, т.к. это просто "записать пустой массив", и это можно сделать как echo "[]" | updater;

  • сохраняет тот же формат файла Info.plist (xml или бинарный).

Использование Command Line Tool на Swift в iOS проекте

Разве что это проблема для тех, кто парсит такие файлы регулярками, но это исправимо.

Использование Command Line Tool на Swift в iOS проекте

Ну и что? Никого же не смущает, что в Dictionary и NSDictionary никогда не было никакого порядка.

Использование Command Line Tool на Swift в iOS проекте

Похожий, но другой пример проблемы:

guard let data = try? Data(contentsOf: url),
			  let infoPlist = String(data: data, encoding: .utf8)
		else { fatalError(.infoPlistNotFound) }

Если do { try f() } catch { fatalError() } и f()! это в принципе одно и то же (потому что одинаково напечатается одинаковая ошибка и одинаково упадёт), то здесь же стало хуже. try! Data(...) напечатал бы гораздо больше полезной информации о том, почему же не получилось прочитать данные, чем просто "infoPlistNotFound". Что ещё и не факт, что правда. Файл-то может быть found, но не смочь прочитаться по тысяче причин: недостаточно прав, прочитался только до половины, да чёрт ещё знает почему. Полезная информация, которая могла бы быть в логах, и помочь расследованию, оказывается просто потеряна.

Использование Command Line Tool на Swift в iOS проекте

Если будет "просто массив строк", то можно его прямо в исходниках и хранить.

Использование Command Line Tool на Swift в iOS проекте

Какой ещё порядок элементов? У словаря нет понятия "порядок элементов".

Использование Command Line Tool на Swift в iOS проекте

Глядя на код в репозитории, я увидел множество проблем с этим примером. Какие-то касаются кода, какие-то более концептуальные. Я даже и не знаю с чего начать.

Начну пожалуй с любимого:

static func fetchDomainsJson(remotePath: String) -> [Domain] {
  guard let url = URL(string: remotePath) else { fatalError(.remotePathIsInvalid) }
  do {
    let data = try Data(contentsOf: url)
    return try JSONDecoder().decode([Domain].self, from: data)
  } catch {
    fatalError(.failedToLoadRemoteFile(error))
  }
}

Обожаю эти do { try f() } catch { fatalError(error) }. Почему бы просто не сделать try! – система точно так же "упадёт", и точно так же напечатает ошибку.

То же касается и url. По-моему, хорошим тоном было бы сразу дать функции нормальные данные (готовый URL), а не заставлять её пытаться из потенциального мусора создать себе нормальное окружение.

Напоследок, раз уж тут есть какие-то try, то пусть бы функция была throws, а делать ли try! пусть решают уровнем выше.

Использование Command Line Tool на Swift в iOS проекте

Я не понимаю, какой массив, из каких строк, и почему что-то нельзя с ним сделать?

Я предлагал читать данные о доменах (видимо для отображения в UI) прямо из Info.plist, причём туда можно записать не только имя домена и флаг NSExceptionAllowsInsecureHTTPLoads, но так же и любые другие пользовательские данные, как тот же description. Да, структура будет чуть другая, ну и что? Вместо массива объектов типа [{ domain, description, flag }] будет структура { domain: { description, flag} }. Попарсить её нет никакой проблемы, зато есть один источник правды.

Использование Command Line Tool на Swift в iOS проекте

Хотя для нас бы не сработало, потому что массив состоял из строк, а не из словарей, куда можно ещё дополнительно закинуть тот же description.

Как это понять?

Использование Command Line Tool на Swift в iOS проекте

Если вы не хотите, чтобы эти домены попадали в релизную сборку, можно включить в Build Settings настройку Preprocess Info.plist File (INFOPLIST_PREPROCESS), добавить в Info.plist Preprocessor Definitions (INFOPLIST_PREPROCESSOR_DEFINITIONS) строчку типа "DEBUG" для Debug версии, и тогда в Info.plist можно будет сделать так:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
#if DEBUG
  <key>NSAppTransportSecurity</key>
  <dict>
    <key>NSExceptionDomains</key>
    <dict>
      <key>mydomain.com</key>
      <dict>
        <key>NSExceptionAllowsInsecureHTTPLoads</key>
        <true/>
        <key>MyDescription</key>
        <string>Hello World</string>
      </dict>
    </dict>
  </dict>
#endif
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
...

Я сейчас впервые в жизни это попробовал сделать, и у меня заработало. Однако, боюсь, что plutil с макросами в теле файла уже не сможет работать. Но я и склоняю к тому, чтобы это менять руками, такие обновления происходят очень редко, и на мой взгляд лучше бы их было видно в истории изменений.

Использование Command Line Tool на Swift в iOS проекте

Но вообще, я не понимаю суть проблемы. Скорее всего это всё находится на подконтрольных серверах, обновляют этот json люди, которые знают и о приложении, и о доменах, и о разработчиках. Info.plist модифицируется при сборке, нужен для отладки. Так может надо просто научить тех, кто меняет домены, пойти в репозиторий приложения и закомитить изменения в Info.plist? Зачем нужна вся эта шайтан-машина со скриптами?

Использование Command Line Tool на Swift в iOS проекте

Кажется, так и есть:

print(Bundle.main.object(forInfoDictionaryKey: "NSAppTransportSecurity"))

Optional({
    NSExceptionDomains =     {
        "mydomain.com" =         {
            MyDescription = "Hello World";
            NSExceptionAllowsInsecureHTTPLoads = 1;
        };
    };
})

Использование Command Line Tool на Swift в iOS проекте

Вместо отдельного "файла-справочника" в приложении, можно читать эти данные прямо из своего же Info.plist. Я подозреваю, что iOS не обидится, если помимо NSExceptionAllowsInsecureHTTPLoads записать ещё и description.

Использование Command Line Tool на Swift в iOS проекте

Ещё рекомендую посмотреть man plutil и man PlistBuddy. PlistBuddy почему-то никогда не попадает в $PATH, но он всегда есть в /usr/libexec/PlistBuddy.

Как я понимаю, задача заточена чисто под iOS, и а раз так, то надо и делать всё так, чтобы было максимально удобно для имеющихся инструментов. Например вместо json с самодельным форматом можно было бы сразу хранить нужную ветку Info.plist в формате plist (или json), и делать PlistBuddy -x "Merge remote.plist". Но у PlistBuddy есть фатальный недостаток: он когда видит несуществующий ключ, возвращает ненулевой код ошибки. Это неудобно.

Но можно сделать почти идеально с plutil: plutil -replace 'NSAppTransportSecurity' -json "{ ... }" -- Info.plist

Вот формат, который мы на самом деле хотим видеть в "настроечном" удалённом файле:

{
   "NSExceptionDomains": {
      "dev.app_main.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : true
      },
      "dev.app_quicksearch.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : true
      },
      "dev.app_cached.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : true
      },

      "stage.app_main.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : false
      },
      "stage.app_quicksearch.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : false
      },
      "stage.app_cached.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : false
      }
   }
}

Остаётся только скачать его и обновить ключ в Info.plist используя plutil:

plutil -replace 'NSAppTransportSecurity' -json "$(curl https://raw.githubusercontent.com/Streetmage/DomainsList/main/remote_domains.json)" -- Info.plist

Чтобы очистить ключ:

plutil -replace 'NSAppTransportSecurity' -json '{}' -- Info.plist

Можно было бы делать и plutil -remove 'NSAppTransportSecurity' -- Info.plist, но plutil тоже будет возвращать код ошибки, если ключа не было, это может быть неудобно.

$ plutil -remove 'NSAppTransportSecurity' -- Info.plist
Info.plist: Could not modify plist, error: No value to remove at key path NSAppTransportSecurity
$ echo $?
1

$ plutil -replace 'NSAppTransportSecurity' -json '{}' -- Info.plist
$ echo $?
0

Использование Command Line Tool на Swift в iOS проекте

У UserDefaults есть разные "domains", и это так называемый Argument Domain: https://developer.apple.com/documentation/foundation/userdefaults/1410665-argumentdomain

Использование Command Line Tool на Swift в iOS проекте

или же можно использовать UserDefaults: аргументы вида "-arg value" доступны из UserDefaults.standard

updater -arg value

print(UserDefaults.standard.value(forKey: "arg")) -> Optional(value)

Использование Command Line Tool на Swift в iOS проекте

но зачем вообще что-то парсить, если есть тот же Environment с именованными параметрами?

URL="https://..."" INFO_PLIST="Info.plist" updater update

Information

Rating
1,454-th
Location
London, England - London, Великобритания
Date of birth
Registered
Activity

Specialization

Mobile Application Developer