Pull to refresh

MSLibrary. Создание и компиляция кроссплатформенных (универсальных) библиотек в Xcode

Reading time 14 min
Views 6K
Очередная статья от команды разработчиков библиотеки MSLibrary for iOS посвящена довольно популярной теме создания и компиляции кроссплатформенных библиотек.

Библиотеки хороши тем, что позволяют использовать разработанный ранее программный код в различных программах. Таким образом, программисты могут существенно упростить себе работу и сократить время ее выполнения, воспользовавшись для части стандартного функционала готовыми решениями, которые входят в состав библиотек. В статье рассматриваются только статические библиотеки, код которых при компиляции полностью входит в состав исполняемого файла, что делает программу легко переносимой.

Мы постарались как можно подробнее и систематичнее осветить вопрос, рассмотрев создание библиотеки «с нуля» и упаковку в библиотеку части функционала уже готового проекта. Под кроссплатформенностью или универсальностью мы подразумевали совместимость библиотеки как с симуляторами, работающими на процессоре с архитектурой i386 или x86_64, так и с реальными девайсами, процессор которых, вероятнее всего, имеет архитектуру armv7 или armv64.

1. Создаем проект библиотеки


При разработке библиотеки перед разработчиком может быть два пути, первый — библиотека создается «с нуля» и второй — в вашем проекте есть некий функционал, на основе которого вы хотите создать библиотеку. Рассмотрим оба.

БИБЛИОТЕКА СОЗДАЕТСЯ «С НУЛЯ»

Последовательность действий такая же, как и при создании нового проекта:

	File > New > Project…
	iOS > Framework & Library > Cocoa Touch Static Library
	Задаем имя (например — MySecretLibrary)

Более подробно как это делается можно посмотреть здесь ↓
Шаг-1. File > New > Project…


рис. 1-1


Шаг-2. Выбираем Cocoa Touch Static Library


рис. 1-2

Шаг-3. Назначаем имя библиотеки (Product Name)…


рис. 1-3


Окно проекта будет выглядеть примерно так:

рис.1-4

БИБЛИОТЕКА СОЗДАЕТСЯ ИЗ ЧАСТИ ФУНКЦИОНАЛА ДРУГОГО ПРОЕКТА

Предположим, что у вас есть готовый проект MyApp и вы хотите на основе нескольких входящих в него классов создать библиотеку. Окно проекта может выглядеть так:


рис. 2-0

Создаем новый таргет для статической библиотеки.
Последовательность действий похожа на последовательность при создании новой библиотеки «с нуля»:

	File > New > Targect…
	iOS > Framework & Library > Cocoa Touch Static Library
	Задаем имя (например — MySecretLibrary)

Более подробно как это делается можно посмотреть здесь ↓
Шаг-1. File > New > Target…


рис. 1-1


Шаг-2. Выбираем Cocoa Touch Static Library


рис. 1-2

Шаг-3. Назначаем имя библиотеки (Product Name)…


рис. 1-3


Окно проекта будет выглядеть примерно так:


рис. 1-4

Вы видите, что в проекте появился новый таргет и папка с файлами MySecretLibrary.

2. Конфигурируем архитектуру библиотеки


По умалчиванию конфигурация приложения включает сборки для armv7 и arm64. Вы можете добавить armv7s (для iPhone 5, iPhone 5c и iPad 2012 года) и еще, например, armv6 и i386. Все зависит от того, на каких платформах библиотека предполагается будет работать.
Для справки приводим. таблицу платформ используемых в различных устройствах Apple в предыдущие годы


рис. 5

3. Конфигурируем Build Phases


ДОБАВЛЯЕМ ФАЙЛЫ ДЛЯ КОМПИЛЯЦИИ

	Build Phases > Complite Sources

По умалчиванию для компиляции включен файл MySecretLibrary.m. Наша задача добавить все необходимые для работы библиотеки файлы, включая служебные заготовочное файлы (.h), ресурсы и тд:


рис. 6-1

ДОБАВЛЯЕМ ЗАГОЛОВОЧНЫЕ ФАЙЛЫ

	Build Phases > Copy Files

По умалчиванию включен файл MySecretLibrary.h. Мы должны добавить все заголовочные файлы, необходимые для подключения библиотеки к новому проекту.

Напомним, что у каждой библиотеки должен быть заголовочный файл, в котором описаны прототипы (объявления) всех функций, содержащихся в этой библиотеке. С помощью заголовочных файлов библиотека сообщаете вашему программному коду, какие функции в ней присутствуют и как их можно использовать.
Важно! Если у вас несколько заголовочных файлов с объявлением различных классов и функций, то нежелательно подключать их все в отдельности к новому проекту. Удобно сделать ОДИН ЗАГОЛОВОЧНЫЙ ФАЙЛ вашей библиотеки и подключить все остальные файлы к нему. Примером могут служить стандартные фреймворки, например Foundation, заголовочный файл которого выглядит так:
Foundation header
/*	Foundation.h
	Copyright (c) 1994-2015, Apple Inc. All rights reserved.
*/


#include <CoreFoundation/CoreFoundation.h>

#import <Foundation/NSObjCRuntime.h>

#import <Foundation/NSArray.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSBundle.h>
#import <Foundation/NSByteOrder.h>
#import <Foundation/NSCalendar.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSCoder.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDate.h>
#import <Foundation/NSDateFormatter.h>
#import <Foundation/NSDateIntervalFormatter.h>
#import <Foundation/NSMassFormatter.h>
#import <Foundation/NSLengthFormatter.h>
#import <Foundation/NSEnergyFormatter.h>
#import <Foundation/NSPersonNameComponents.h>
#import <Foundation/NSPersonNameComponentsFormatter.h>
#import <Foundation/NSDecimal.h>
#import <Foundation/NSDecimalNumber.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSError.h>
#import <Foundation/NSException.h>
#import <Foundation/NSFileHandle.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSFormatter.h>
#import <Foundation/NSHashTable.h>
#import <Foundation/NSHTTPCookie.h>
#import <Foundation/NSHTTPCookieStorage.h>
#import <Foundation/NSIndexPath.h>
#import <Foundation/NSIndexSet.h>
#import <Foundation/NSInvocation.h>
#import <Foundation/NSJSONSerialization.h>
#import <Foundation/NSKeyValueCoding.h>
#import <Foundation/NSKeyValueObserving.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSLocale.h>
#import <Foundation/NSLock.h>
#import <Foundation/NSMapTable.h>
#import <Foundation/NSMethodSignature.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSNotificationQueue.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSNumberFormatter.h>
#import <Foundation/NSObject.h>
#import <Foundation/NSOperation.h>
#import <Foundation/NSOrderedSet.h>
#import <Foundation/NSOrthography.h>
#import <Foundation/NSPathUtilities.h>
#import <Foundation/NSPointerArray.h>
#import <Foundation/NSPointerFunctions.h>
#import <Foundation/NSPort.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSPropertyList.h>
#import <Foundation/NSProxy.h>
#import <Foundation/NSRange.h>
#import <Foundation/NSRegularExpression.h>
#import <Foundation/NSRunLoop.h>
#import <Foundation/NSScanner.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSSortDescriptor.h>
#import <Foundation/NSStream.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTextCheckingResult.h>
#import <Foundation/NSThread.h>
#import <Foundation/NSTimeZone.h>
#import <Foundation/NSTimer.h>
#import <Foundation/NSURL.h>
#import <Foundation/NSURLAuthenticationChallenge.h>
#import <Foundation/NSURLCache.h>
#import <Foundation/NSURLConnection.h>
#import <Foundation/NSURLCredential.h>
#import <Foundation/NSURLCredentialStorage.h>
#import <Foundation/NSURLError.h>
#import <Foundation/NSURLProtectionSpace.h>
#import <Foundation/NSURLProtocol.h>
#import <Foundation/NSURLRequest.h>
#import <Foundation/NSURLResponse.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSValue.h>
#import <Foundation/NSValueTransformer.h>
#import <Foundation/NSXMLParser.h>
#import <Foundation/NSZone.h>

#import <Foundation/FoundationErrors.h>

#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)

#import <Foundation/NSAttributedString.h>
#import <Foundation/NSByteCountFormatter.h>
#import <Foundation/NSCache.h>
#import <Foundation/NSComparisonPredicate.h>
#import <Foundation/NSCompoundPredicate.h>
#import <Foundation/NSDateComponentsFormatter.h>
#import <Foundation/NSExpression.h>
#import <Foundation/NSExtensionContext.h>
#import <Foundation/NSExtensionItem.h>
#import <Foundation/NSExtensionRequestHandling.h>
#import <Foundation/NSFileCoordinator.h>
#import <Foundation/NSFilePresenter.h>
#import <Foundation/NSFileVersion.h>
#import <Foundation/NSFileWrapper.h>
#import <Foundation/NSItemProvider.h>
#import <Foundation/NSLinguisticTagger.h>
#import <Foundation/NSMetadata.h>
#import <Foundation/NSMetadataAttributes.h>
#import <Foundation/NSNetServices.h>
#import <Foundation/NSPredicate.h>
#import <Foundation/NSProgress.h>
#import <Foundation/NSUbiquitousKeyValueStore.h>
#import <Foundation/NSUndoManager.h>
#import <Foundation/NSURLSession.h>
#import <Foundation/NSUserActivity.h>
#import <Foundation/NSUUID.h>
#endif

#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || TARGET_OS_WIN32

#import <Foundation/NSArchiver.h>
#import <Foundation/NSBackgroundActivityScheduler.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSConnection.h>
#import <Foundation/NSDistantObject.h>
#import <Foundation/NSDistributedNotificationCenter.h>
#import <Foundation/NSGeometry.h>
#import <Foundation/NSPortCoder.h>
#import <Foundation/NSPortMessage.h>
#import <Foundation/NSPortNameServer.h>
#import <Foundation/NSProtocolChecker.h>
#import <Foundation/NSTask.h>
#import <Foundation/NSXMLDTD.h>
#import <Foundation/NSXMLDTDNode.h>
#import <Foundation/NSXMLDocument.h>
#import <Foundation/NSXMLElement.h>
#import <Foundation/NSXMLNode.h>
#import <Foundation/NSXMLNodeOptions.h>
#import <Foundation/NSURLDownload.h>
#import <Foundation/NSURLHandle.h>

#endif

#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))

#import <Foundation/NSAffineTransform.h>
#import <Foundation/NSAppleEventDescriptor.h>
#import <Foundation/NSAppleEventManager.h>
#import <Foundation/NSAppleScript.h>
#import <Foundation/NSClassDescription.h>
#import <Foundation/NSDistributedLock.h>
#import <Foundation/NSGarbageCollector.h>
#import <Foundation/NSHFSFileTypes.h>
#import <Foundation/NSHost.h>
#import <Foundation/NSObjectScripting.h>
#import <Foundation/NSScriptClassDescription.h>
#import <Foundation/NSScriptCoercionHandler.h>
#import <Foundation/NSScriptCommand.h>
#import <Foundation/NSScriptCommandDescription.h>
#import <Foundation/NSScriptExecutionContext.h>
#import <Foundation/NSScriptKeyValueCoding.h>
#import <Foundation/NSScriptObjectSpecifiers.h>
#import <Foundation/NSScriptStandardSuiteCommands.h>
#import <Foundation/NSScriptSuiteRegistry.h>
#import <Foundation/NSScriptWhoseTests.h>
#import <Foundation/NSSpellServer.h>
#import <Foundation/NSUserNotification.h>
#import <Foundation/NSUserScriptTask.h>
#import <Foundation/NSXPCConnection.h>

#endif



рис. 6-2

При создании библиотеки из готового приложения, необходимо совершить те же действия по настройке Build Phases. При этом надо понимать, что Complite Sours и Copy Files могут содержать не только файлы из группы MySecretLibrary, созданной одноименным таргетом, но и любые другие ресурсы вашего приложения. Группа MySecretLibrary создается исключительно для того, чтобы дать вам возможность маневра, к примеру перенести в находящиеся там файлы только отдельные классы и функции вашего приложения. Здесь следует напомнить, что при компиляции Xcode использует только те файлы, которые указаны в Build Phases именно для этого таргета. То есть в приведенном примере будут компилироваться только файлы из папки MySecretLibrary.

В принципе на этом этапе вся подготовительная работа выполнена и можно приступить к сборке библиотеки… Но, здесь существует одна тонкость. Дело в том, что Xcode компилирует продукт исходя из актуальный настройки, либо для симулятора, запущенного на вашем компьютере, а следовательно работающего на процессоре с архитектурой i386 или x86_64, либо для девайса, процессор которого, вероятнее всего, имеет архитектуру armv7, armv7s или armv64. Таким образом скомпилированная под симулятор библиотека не будет работать на реальных девайсах и наоборот, скомпилированная для девайсов не будет работать на симуляторах.

КОМПИЛИРУЕМ ДЛЯ СИМУЛЯТОРА

Устанавливаем В качестве «active scheme» таргет библиотеки — MySecretLibrary, выбираем симулятор, например MySecretLibrary > iPhone 6s


рис. 7-1

и нажимаем сочетание клавиш ⌘B, чтобы скомпилировать проект библиотеки.

В результате получится так, как изображено на картинке:


рис. 7-2

Обратите внимание, что название файла libMySecretLibrary.a из красного стало черным, то есть компиляция прошла успешно и файл был сохранен на вашем компьютере. Если теперь щелкнуть «правой кнопкой», появится меню в котором выбираем пункт Show in Finder.


рис. 7-3

В открывшемся окне мы увидим папку Debug-iphonesimulator в которой находятся файл библиотеки — libMySecretLibrary.a и заготовочный файл — MySecretLibrary.h


рис. 7-4

Если перейти на папку выше, то будет понятно, что папка собранной для симулятора библиотеки находится в папке Products.


рис. 7-5

Полный путь до папки Debug-iphonesimulator в нашем случае:

/Users/[userName]/Library/Developer/Xcode/DerivedData/MySecretLibrary-esufqblgaisjjeecltzoxyiyogzj/Build/Products/Debug-iphonesimulator

КОМПИЛИРУЕМ ДЛЯ ДЕВАЙСА

Устанавливаем В качестве «active scheme» таргет библиотеки — MySecretLibrary, выбираем «Generic iOS Device» (MySecretLibrary > Generic iOS Device)


рис. 8-1

и нажимаем сочетание клавиш ⌘B, чтобы скомпилировать проект библиотеки.


рис. 8-2

Выбираем > Show in Finder:


рис. 8-3

Переходим в папку библиотеки. Она аналогична предыдущей, но файл библиотеки libMySecretLibrary.a скомпилирован для архитектуры девайса.


рис. 8-4

Теперь папка Products выглядит так:


рис. 8-5

Полный путь до папки Debug-iphoneos в нашем случае:

/Users/[userName]/Library/Developer/Xcode/DerivedData/MySecretLibrary-esufqblgaisjjeecltzoxyiyogzj/Build/Products/Debug-iphoneos

Видно, что в папке Products находятся две библиотеки — для симулятора и для девайса.

Следующий шаг — создать один бинарный файл, объединяющий оба варианта библиотеки.

4. Генерируем универсальный бинарный файл


Предлагается довольно много вариантов создания универсального бинарного файла, но во всех случаях суть сводится к трем последовательным действиям:
— генерируем библиотеку для девайса
— генерируем библиотеку для симулятора
— объединяем библиотеки и создаем бинарный файл с помощью команды lipo

Для интересующихся приводим полное описание команды lipo ↓
lipo
Create or operate on a universal file: convert a universal binary to a single architecture file, or vice versa.
Syntax
      lipo [input_file]... [-arch arch_type input_file]... [-arch_blank arch_type]...
              { -info | -detailed_info }
                 [-output output_file] Operation
                    [-segalign arch_type value]

        where Operation is one of:
           [-create] [-arch_blank arch_type]
           [-thin arch_type]
           [-extract arch_type] ...
           [-extract_family arch_type] ...
           [-remove arch_type] ...
           [-replace arch_type file_name] ...
           [-verify_arch arch_type ...]

Options
   -info  Briefly list the architecture types in the input universal file.
          Lists the names of each archive.

   -detailed_info
          Display a detailed list of the architecture types in the input universal file.
          Lists universal header info. for each architecture in the file.

   -arch arch_type input_file
          Tell  lipo  that  input_file  contains  the specified architecture type. 
          This is unnecessary if input_file is an object file, a universal file,
          or some other file whose architecture(s) lipo can figure out.

   -arch_blank arch_type
          The output for the specified arch_type will be an MH_DYLIB_STUB file.
          This flag can not be used with any operation other than -create.

   -output output_file
          The output file.

   -create
          Create one universal output file from the input file(s).

   -thin arch_type
          Take one input file and create a thin output file with the specified arch_type.

   -replace arch_type file_name
          Take one universal input file; in the output file, replace the arch_type contents 
          of the input file with the contents of the specified file_name.

   -remove arch_type
          Take  one  universal input file and remove the arch_type from that universal file, 
          placing the result in the output file.

   -extract arch_type
          Take one universal input file and copy the arch_type from that universal file into 
          a universal output file containing only that architecture.

   -extract_family arch_type
          Take one universal input file and copy all of the arch_types for the family that
          arch_type is in from that universal file into an output file containing only those
          architectures.  If only one architecture is found the file will be thin.

   -verify_arch arch_type ...
          Take one input file and verify the specified arch_types are present in the file. 
          If so then exit with a status of 0 else exit with a status of 1.

   -segalign arch_type value
          Set the segment alignment of the specified arch_type when creating a universal file 
          containing that  architecture.   value is a hexadecimal number that must be an 
          integral power of 2.  This is only needed when lipo can't figure out the alignment 
          of an input  file  (currently  not  an object  file),  or when it guesses at the
          alignment too conservatively.  The default for files unknown to lipo is 0
          (2^0, or an alignment of one byte), and the  default  alignment  for  archives 
          is 4 (2^2, or 4-byte alignment).

arch_type A supported  architecture name: i386 (32-bit intel)  ppc (32-bit powerpc)
          ppc64 (64-bit powerpc) or x86_64 (64-bit intel)
lipo produces one output file, and never alters the input file.
lipo can: list the architecture types in a universal file; create a single universal file from one or more input files; thin out a single universal file to one specified architecture type; and extract, replace, and/or remove architectures types from the input file to create a single new universal output file.
Example
$ cd /Applications
$ lipo Stickies.app/Contents/MacOS/Stickies -info

$ lipo Stickies.app/Contents/MacOS/Stickies -thin i386 -output Stickies.app/Contents/MacOS/Stickies.i386
$ cd Stickies.app/Contents/MacOS/
$ rm Stickies
$ mv Stickies.i386 Stickies


Подробнее о различных командах (comand line) можно почитать здесь SS64.com

СОЗДАЕМ УНИВЕРСАЛЬНЫЙ БИНАРНЫЙ ФАЙЛ БИБЛИОТЕКИ

— Компилируем библиотеку для девайса (как описано выше)
— Компилируем библиотеку для симулятора (как описано выше)
— Открываем Терминал
— Переходим в Терминале в папку Products (команда cd и путь к папке)
— Вводим следующий текст:

lipo -create Debug-iphoneos/libMySecretLibrary.a Debug-iphonesimulator/libMySecretLibrary.a -output libMySecretLibrary_combined.a

В результате мы получаем файл универсальной библиотеки libMySecretLibrary_combined.a, сохраненный в папке Products. Заголовочный файл можно использовать из папок Debug-iphoneos или Debug-iphonesimulator.


рис. 9-0

Естественно, что вы должны использовать в коде название ваших библиотек вместо libMySecretLibrary.

Собственно на этом задача выполнена. Однако, Xcode предлагает инструменты для автоматизации ряда процессов, в частности можно автоматизировать создание и сборку библиотек с помощью таргета Aggregate.

ДОБАВЛЯЕМ В ПРОЕКТ НОВЫЙ ТАРГЕТ — AGGREGATE

В Xcode существует особый шаблон таргетов, которые не обязательно
соответствуют конкретному типу продукта. Это специальные таргеты Aggregate, которые могут быть использованы для:
— Объединения группы таргетов
— Копирование файлов в определенное место файловой системы
— Создание продукта, используя внешнюю систему сборки
— Запуск командного скрипта

В нашем случае, мы создадим таргет для выполнения командного скрипта с помощью которого будет собрана универсальная библиотека, объединяющая два слоя — для симулятора и для девайса.

Последовательность действий такая же, как и при добавлении других таргетов:

	File > New > Targect…
	Other (в самом низу после OS X) > Aggregate
	Задаем имя (например — MySecretLibrary_combined)

Более подробно как это делается можно посмотреть здесь ↓
Шаг-1. File > New > Target…

рис. 9-1

Шаг-2. Выбираем Other > Aggregate 


рис. 9-2

Шаг-3. Назначаем имя библиотеки (Product Name)… 


рис. 9-3


После этого окно проекта будет выглядеть примерно так:


рис. 9-4

КОНФИГУРИРУЕМ AGGREGATE (добавляем форму для размещения командного скрипта)

Выделяем таргет Aggregate, переходим в раздел > Build Phases и нажимаем на значок "+"


рис. 10-1

В открывшемся меню выбираем > New Run Script Phase


рис. 10-2

Открывается форма, для размещения скрипта, управляющего сборкой библиотеки.
Окно проекта должно выглядеть примерно так (стрелкой указано место куда следует разместить скрипт):


рис. 10-3

ДОБАВЛЯЕМ КОМАНДНЫЙ СКРИПТ

Командный скрипт для создания универсальной библиотеки:

BUILD_TARGET_DIR=$PROJECT_DIR/build
xcodebuild -target $PROJECT_NAME -sdk "iphonesimulator" -configuration "Release" clean build
xcodebuild -target $PROJECT_NAME -sdk "iphoneos" -configuration "Release" clean build
lipo -create -output "$BUILD_TARGET_DIR/$PROJECT_NAME-Combined.a" "$BUILD_TARGET_DIR/Release-iphoneos/lib$PROJECT_NAME.a" "$BUILD_TARGET_DIR/Release-iphonesimulator/lib$PROJECT_NAME.a"

Видно, что скрипт автоматизирует выполнение тех же трех простых действий:
— генерирует библиотеку для симулятора
— генерирует библиотеку для девайса
— объединяет библиотеки и создает бинарный файл с помощью команды lipo

Копируем этот скрипт и добавляем в форму > Run Script:


рис. 10-4

Устанавливаем в качестве «active scheme» таргет aggregate MySecretLibrary


рис. 10-5

Нажимаем ⌘B, чтобы скомпилировать проект библиотеки.

В результате в папке вашего проекта (не в папке Products!) появится папка build в которой находятся все сформированные файлы.


рис. 10-6

Вот, собственно, и все. Как видите вопрос решается всего в несколько простых строк кода.

За пределами статьи остались вопросы связанные с устранением дублирования связанных ресурсов и актуализацией библиотек, уже установленных в приложения. Последнее время большую популярность приобретают системы управления зависимостями типа CocoaPods. Использование таких систем вопрос неоднозначный. С одной стороны появляется возможность избежать дублирования ресурсов и поддерживать актуальную версию библиотеки в готовых приложениях. С другой — полная зависимость от репозиториев в которых хранятся библиотеки и которые принадлежат третьим лицам и кроме того возможность «падения» приложения, если новая версия библиотеки не полностью совместима с предыдущей.



Надеемся, что материал был для вас полезен, команда MSLibrary for iOS

Другие статьи:
Захват и верификация телефонных номеров с помощью регулярных выражений, для iOS и не только… Часть 1
Захват и верификация телефонных номеров с помощью регулярных выражений, для iOS и не только… Часть 2
Реализация множественного выбора условий с помощью битовых масок, для iOS и не только…
ПРОСТО: удаляем из строки ненужные символы, используя регулярные выражения, для iOS и не только…
Tags:
Hubs:
+6
Comments 3
Comments Comments 3

Articles