스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift 패키지 플러그인 만들기
개발 작업 흐름을 맞춤화하고 Swift에서 나만의 패키지 플러그인을 작성하는 방법을 배울 수 있습니다. PackagePlugin API를 사용하여 Xcode의 기능을 확장함으로써 소스 코드를 생성하거나 릴리즈 작업을 자동화하는 방법을 보여드리며 우수한 플러그인을 만들기 위한 모범 사례를 소개합니다.
리소스
관련 비디오
WWDC22
WWDC21
WWDC20
WWDC19
-
다운로드
안녕하세요 제 이름은 Boris입니다 'Swift 패키지 플러그인 만들기' 세션에 오신 것을 환영합니다
Xcode 11에서는 Swift 패키지 지원이 도입되었습니다 라이브러리를 소스 코드로 간단히 배포할 수 있었죠 Xcode 14에서도 같은 방식으로 구성 요소들을 개발 워크플로에 구조화하고 공유하고자 합니다 예를 들면 소스 코드 만들기 및 릴리즈 작업 자동같은 거죠 Swift 패키지 플러그인으로 할 수 있습니다 먼저, 개요를 간단히 말씀드리겠습니다 플러그인의 기초를 배우고 사용자 정의 명령 플러그인 빌드를 보여드리겠습니다 그리고 플러그인 만들기를 자세히 살펴보겠습니다 그리고 추가 데모에서 인빌드 및 사전 빌드 명령 플러그인을 모두 빌드하겠습니다
패키지 플러그인은 PackagePlugin API를 사용하는 Swift 코드입니다 패키지 매니페스트와 비슷하죠 플러그인은 확장 포인트를 잘 정의하여 Xcode 또는 Swift 패키지 매니저의 기능을 확장할 수 있습니다
그러면 패키지 플러그인은 어떻게 작동할까요? Xcode는 플러그인을 컴파일하고 실행합니다 플러그인은 사용 가능한 실행 파일과 입력 파일에 대한 정보를 사용하여 명령을 구성합니다 그리고 필요에 따라 실행하기 위해 Xcode에게 다시 보냅니다
패키지 플러그인은 빌드 전이나 빌드 중에 실행되는 사용자 정의 빌드 작업에 사용될 수 있습니다 예를 들어 소스 코드나 리소스 파일을 만드는 거죠
또한 SwiftPM의 명령줄 인터페이스나 Xcode의 메뉴 항목에 사용자 정의 명령을 추가할 수 있습니다 플러그인에 대해서는 'Swift 패키지 플러그인 만나기'에서 기본 정보를 얻을 수 있습니다 여러분이 패키지를 완전히 처음 사용하는 경우라면 WWDC19 세션에서 'Swift 패키지 만들기'를 보시면 좋을 겁니다
이제 사용자 정의 명령 플러그인을 빌드하는 방법을 알아보겠습니다
저는 Swift 오픈 소스에서 tools-support-core 패키지로 작업 중입니다 프로젝트에 기여한 사람들이 모두 적힌 텍스트 파일을 추가하려고 합니다 또한 이 파일을 패키지 Git 히스토리에서 필요할 때마다 다시 만들고 싶어요
이전에는 쉘 스크립트나 makefile로 했을 거에요 하지만 지금은 사용자 정의 명령 플러그인을 만들겠습니다 Xcode를 벗어나지 않고도 파일을 다시 만들 수 있도록요
먼저 플러그인의 디렉토리 구조를 만들어야 합니다 패키지에서 Context 메뉴를 엽니다 New Folder를 선택합니다 가장 위에 Plugins 폴더를 만듭니다 기존 소스 테스트와 비슷하죠
그리고 폴더 아래에 다른 폴더를 만듭니다 플러그인 타겟용으로 GenerateContributors라는 폴더를 만듭니다
그 안에 plugin.swift라는 새 파일을 만듭니다
다음으로, 새로운 타겟을 선언하기 위해 패키지 매니페스트를 좀 바꿀게요 하지만 먼저 패키지의 도구 버전을 5.6으로 올려야 합니다 플러그인은 5.6 버전 이상에서만 사용할 수 있기 때문입니다
이제 플러그인 타겟을 삽입할 수 있습니다
새 매니페스트 API를 살펴봅시다
우리는 Plugins 폴더 안의 폴더에서 플러그인 타겟을 만들고 있죠 소스 모듈 타겟과 비슷합니다
Xcode의 메뉴 항목 뿐만 아니라 폴더 이름에 관련된 이름을 갖습니다
기능을 특정합니다 우리가 사용하고 싶은 확장 포인트 유형을 알려주는 거죠 이 경우에는 사용자 정의 명령을 만들고 있죠
intent에서 SwiftPM 명령줄에 대한 동사를 정의하고 플러그인이 하는 일에 대해 설명할 수 있습니다 마지막으로 플러그인에 필요한 권한을 선언할 수 있습니다
이 경우에는 패키지의 루트를 통해 새 파일을 쓰고 싶죠 그래서 그 디렉토리에 쓸 수 있는 권한이 필요합니다 플러그인 사용자에게 reason 문자열이 표시됩니다 권한을 부여할지 여부를 알 수 있도록요 OS에서 권한이 작동하는 방식과 비슷하죠 이제 플러그인을 선언했으니까 다시 구현해 보겠습니다
플러그인은 커밋 히스토리를 얻기 위해 Git에 보내고 외부 Git 명령의 standardout에서 히스토리를 읽고 결과를 파싱하고 마지막으로 그걸 텍스트 파일에 씁니다
이전에 만든 플러그인 소스 파일을 열어서 패키지 플러그인을 임포트합니다
이것은 내장 모듈입니다 플러그인을 구현하는 데 사용할 수 있는 API에 대한 액세스를 제공하는 PackageDescription과 유사하죠
GenerateContributors 구조체를 정의하고 CommandPlugin을 따르게 합니다
fix-it을 수락합니다 프로토콜을 구현하기 위해 누락된 스텁을 가져옵니다 또한 구조체를 @main으로 표시합니다 플러그인 실행 파일의 main 함수가 될 거니까요
명령의 엔트리 포인트는 performCommand입니다 두 개의 인수를 받습니다 컨텍스트는 분해된 패키지 그래프와 우리가 실행하는 컨텍스트에 대한 정보에 접근할 수 있게 해줍니다 인수뿐만 아니라요 사용자 정의 명령은 사용자가 호출하므로 인수 형식으로 입력을 제공할 수 있습니다 우리는 간단한 명령을 만들고 있으니까 실제로 사용자에게는 어떤 옵션도 주지 않을 겁니다
커밋 히스토리에 대한 정보를 얻기 위해 git으로 실행하고 싶으니까 Foundation을 임포트해야 합니다 왜냐하면 Process API가 필요하기 때문입니다
다음으로 프로세스 인스턴스를 정의하고, git log와 형식 인수를 실행하도록 설정합니다
프로세스 출력을 캡처하려면 파이프를 생성해야 합니다 그리고 실행하고 종료될 때까지 기다립니다
프로세스가 완료되면 파이프에서 모든 데이터를 읽어서 git log로 출력된 것들이 들어있는 문자열로 변환합니다
문자열을 조금 조작합니다 중복되지 않은 목록으로 출력하기 위해 트리밍하는 거죠 이렇게 하면 마침내 contributors.txt에 쓸 수 있습니다 사용자 정의 명령은 패키지의 루트 디렉토리에서 실행되기 때문에 거기에 파일을 저장하겠습니다
이제 저장하고 프로젝트 탐색기에서 패키지를 마우스 오른쪽 클릭하면 컨텍스트 메뉴에 새 명령 항목이 생깁니다 실행해 봅시다
대화 상자에서 패키지 또는 타겟을 선택할 수 있습니다 인수 또는 플러그인에 대한 입력이 되겠죠 플러그인은 이런 옵션에 반응하지 않으니까, 실행을 클릭합니다
그리고 앞에서 매니페스트에서 정의한 대로 권한을 요청합니다 우리가 플러그인을 만들었으니까 그냥 실행하면 됩니다 하지만 추가 권한은 신뢰 가능한 플러그인에만 부여해야 합니다
실행하면 contributors.txt 파일이 프로젝트 탐색기에 나타납니다
이제 Xcode를 첫 번째 플러그인으로 확장했으니 플러그인 작동 방식과, 플러그인을 만들 때 주의할 점에 대해 조금 더 자세히 살펴보겠습니다
패키지 플러그인은 샌드박스에서 실행되며 패키지 매니페스트 자체의 평가와 비슷합니다 임시 위치가 아닌 네트워크에서는 플러그인의 작업 디렉토리에만 액세스하거나 쓸 수 있습니다 하지만 사용자 정의 명령은 이전에 보여드린 대로 패키지의 루트 디렉토리에 쓰고 싶다고 선언할 수 있습니다 기존 써드파티 도구를 래핑하는 경우라면 샌드박스 모델 안에 넣는 방법을 알아보셔야 할 수도 있어요 예를 들면 생성된 파일이 기록되는 곳을 구성해서요
도입부에서 다양한 유형의 플러그인에 대해 이야기했지만 사용자 정의 명령 또는 빌드 도구 중 어느 것을 통해 더 잘 해결되는지 명확해야 합니다 그러면 빌드 도구 플러그인의 구조를 살펴보겠습니다
이 플러그인을 사용하면 빌드 시스템을 확장할 수 있습니다 빌드 중에 실행할 실행 파일에 대한 설명을 제공하고 입력과 출력을 지정함으로써요 그러면 빌드 중 적절한 시간에 작업을 예약하는 데 도움이 됩니다
여러분 중에는 Xcode 프로젝트에서 실행 단계 스크립트를 만드는 기본 개념에 대해 잘 알고 있는 분도 계실 겁니다
빌드 도구 플러그인에는 두 가지 유형이 있습니다 중요한 것은, 여러분의 도구에 정의된 출력 세트가 있는지 입니다
만약 있다면 인빌드 명령을 만들어야 합니다 입력에 비해 출력이 오래된 경우 빌드 시스템은 인빌드 명령을 자동으로 다시 실행합니다 명확한 출력 세트가 없다면 사전 빌드 명령을 만들 수 있습니다 빌드가 시작될 때마다 실행되겠죠 따라서 사전 빌드 명령에서 비용이 많이 드는 작업을 할 때는 조심해야 합니다 아니면 여러분의 사용 사례에 적합한, 결과를 캐싱하기 위한 맞춤형 전략을 생각해 내야 합니다
두 번째 데모에서는 아이콘을 캡슐화하는 새 라이브러리를 만들게요 이 아이콘들을 제가 사용하는 다른 도구들에서 공유하고 싶어요
시작해 봅시다, 템플릿에서 새 패키지를 만들겠습니다 IconLibrary라고 하겠습니다 이미 갖고 있는 아이콘 에셋을 라이브러리 타겟으로 끌어옵니다 기본 SwiftUI 보기와 미리 보기를 내 라이브러리에 추가하겠습니다 먼저 최소 배포 타겟을 매니페스트에 추가해야 합니다
그리고 기본 보기와 미리 보기를 실제로 추가합니다 방금 전에 끌어다 놓은 에셋을 사용할 수 있습니다
제 생각엔 여기서 문자열을 처리하는 대신 이미지를 참조하는 유형 안정 방식으로 하는 게 좋을 것 같아요 이건 인빌드 명령 플러그인의 훌륭한 사용 사례일 겁니다 에셋 카탈로그를 살펴보고 그것들을 기반으로 몇 가지 Swift 코드를 만들겠습니다 Finder에서 에셋 카탈로그를 살펴보겠습니다 플러그인의 필요 정보를 추출하는 방법을 알아보기 위해서요
각 이미지는 에셋명이 있는 자체 이미지 세트 디렉토리를 갖습니다
그리고 기본 내용을 설명하는 JSON 파일도 있습니다
인빌드 명령은 사용자 정의 명령과 약간 다르게 작동합니다 입력과 출력 뿐만 아니라 실행할 실행 파일에 대한 설명도 있습니다
실행 파일은 시스템이나 써드 파티 패키지에서 제공될 수 있습니다 또는 여러분의 플러그인에 맞게 만드셔도 됩니다 여기서는 세 번째 방식으로 해볼게요
플러그인은 빌드 프로세스가 시작될 때 실행됩니다 빌드 그래프를 계산하기 위해서요
이를 기반으로, 실행 파일은 빌드 중에 실행되도록 예약됩니다
이제 우리가 만들고 있는 실행 파일로 돌아갑시다 에셋 카탈로그의 각 이미지에 대해 컴파일 시간 상수가 필요합니다 각 이미지에 대한 올바른 문자열을 기억할 필요 없이 Swift 기호로 자동 완성될 수 있도록요
에셋 카탈로그의 디렉토리 내용을 반복하면서 모든 이미지 세트를 찾고 싶습니다 각 이미지 세트에 대해 메타데이터를 파싱하여 실제로 이미지가 포함된 경우를 찾아 그에 대한 코드를 만들어야 하니까요
그런 다음, 코드를 만들어서 파일에 쓸 수 있습니다 그 파일을 플러그인의 출력으로 선언했기 때문에 플러그인이 적용된 타겟의 빌드에 자동으로 통합됩니다
인수를 처리해야 합니다 플러그인과 실행 파일 간의 통신 방법이기 때문입니다
첫 번째 인수는 처리 중인 에셋 카탈로그의 경로입니다 두 번째 인수는 생성된 코드에 대해 플러그인이 제공하는 경로입니다
Contents JSON 파일 디코딩을 위한 모델 객체가 몇 개 필요합니다
Decodable을 사용하여 Swift의 내장 JSON 디코딩을 활용합니다
우리가 관심을 갖는 유일한 정보는 이미지 목록과 파일 이름입니다 이건 선택 사항인데요, 각 픽셀 밀도마다 이미지가 존재하지는 않을 수도 있어서요 여기서는 간단한 방식으로 코드를 생성하겠습니다 그냥 문자열을 빌드할게요 필요한 프레임워크부터 가져오겠습니다 Foundation과 SwiftUI입니다
에셋 카탈로그의 디렉토리 콘텐츠에서 루프를 반복하고 싶습니다 모든 이미지 세트를 찾기 위해서요 다음으로 JSON을 파싱해야 합니다 파일 이름은 input 매개변수를 사용합니다 Foundation의 JSONDecoder API를 사용하여 디코딩합니다
우리가 관심을 갖는 주요 정보는 주어진 이미지 세트에 대해 정의된 이미지가 있는지 여부입니다 비어 있지 않은 파일 이름에 이미지가 하나 이상 있는지 확인하여 결정합니다 주어진 이미지 세트에 이미지가 있으면 패키지의 번들에서 이미지를 로드하는 SwiftUI 이미지를 생성하고 싶습니다
각 이미지의 기본 이름으로 문자열을 빌드하여 모듈 번들에서 주어진 이미지를 로드합니다 모듈 번들은 리소스가 있는 각 패키지에 대해서 빌드 시스템이 생성하는 리소스 번들입니다
생성된 코드를 파일에 써서 실행 파일 작업을 끝냅니다 인수를 통해 주어진 거죠
그럼 Xcode로 돌아가 실행 파일을 만들어 보겠습니다
AssetConstantsExec이라고 할게요
main 파일을 추가합니다
이제 패키지 매니페스트에서 선언해야 합니다
main 파일에 방금 얘기한 코드를 추가합니다
이제 코드를 생성할 수 있는 실행 파일이 있으므로 플러그인을 사용하여 빌드 시스템으로 가져올 수 있습니다
필요한 타겟을 추가해 봅시다 그리고 Plugin Usage를 라이브러리 타겟에서 추가합니다
이전처럼 PackagePlugin 라이브러리를 임포트합니다 그리고 구조체를 만듭니다 이번에는 빌드 도구 플러그인 프로토콜을 따릅니다
엔트리 포인트는 비슷해 보이지만 사용자 인수 대신 타겟을 받습니다 플러그인이 적용되는 타겟이죠 엔트리 포인트는 플러그인을 사용하는 타겟당 한 번 호출됩니다
이 플러그인은 특히 소스 모듈 타겟을 다루는데요 예를 들어 바이너리 타겟과 대조적으로, 실제로 소스 파일을 전달하는 모든 타겟을 말합니다 빌드 명령 배열을 빌드하기 위해 타겟의 모든 XCAssets 번들에서 루프를 반복합니다 빌드 로그에 표시되는 이름에 대한 문자열을 추출하고요 그리고 적절한 입출력 경로를 구성합니다 플러그인 API를 사용하여 여기에서 실행 파일을 찾을 수도 있습니다 그런 다음 빌드 명령들을 합칩니다
이렇게 해서 프로젝트를 다시 빌드할 준비가 되었습니다 실행되는 새 빌드 단계에 대한 빌드 로그를 살펴볼 수 있습니다
플러그인은 빌드가 시작될 때 컴파일되고 실행됩니다 여기에서 생성된 명령을 빌드 그래프에 추가합니다
타겟을 보면 새 빌드 명령이 실행되었음을 볼 수 있습니다
마지막으로 소스 파일이 생성되어 Swift 파일 컴파일에 나타납니다
다시 미리 보기로 돌아가 보겠습니다 문자열 형식의 이미지 구성을 새 상수로 바꿀 수 있습니다
다른 이미지 이름에 대한 자동 완성 기능도 있습니다
이건 좋은 거죠 비교적 적은 코드로 워크플로를 개선할 수 있습니다 익숙한 Swift API를 사용하여 Xcode로 할 수 있습니다
우리가 사용할 플러그인을 만드는 방법을 살펴보았습니다 이미 작업하고 있던 라이브러리에 포함시켜서요 그러나 플러그인의 또 다른 강력한 속성은 간단하게 공유할 수 있다는 입니다 라이브러리와 비슷하죠
다음 데모에서는 사전 빌드 절차의 일부를 자동화해볼게요 Xcode와 함께 제공되는 genstrings 도구를 사용합니다 이 도구는 코드에서 로컬라이즈된 문자열을 나중에 사용하기 위해 로컬 디렉토리로 추출합니다 일반적으로 유용할 것 같으니까 플러그인을 별도의 패키지로 만들고 싶습니다 독립적으로 공유될 수 있도록요
패키지의 리소스 및 로컬라이즈에 대해 더 자세히 알고 싶다면 WWDC20 세션을 추천합니다 로컬라이제이션 전반에 대한 자세한 내용은 WWDC21에서 'SwiftUI 앱 로컬라이즈하기'를 확인하세요
이 플러그인은 로컬라이제이션을 위한 출력 디렉토리를 계산하면서 시작하겠습니다 입력 파일을 계산합니다 주어진 타겟에 있는 모든 Swift 또는 Objective-C 소스 파일이죠 그런 다음 Xcode에서 제공하는 genstrings 도구를 실행하기 위해 사전 빌드 명령을 구성합니다 사전 빌드 명령과 인빌드 명령의 가장 큰 차이점은 잘 정의된 출력 세트를 선언하지 않는다는 것입니다 즉, 이 명령들은 모든 빌드에서 실행된다는 뜻이죠
이 도구는 사용자의 소스 코드에서 모든 로컬라이즈된 문자열을 추출합니다 그런 다음 모든 문자열을 로컬 디렉토리에 씁니다 사용자의 프로젝트에서 실제 로컬라이제이션 작업의 기초로 사용할 수 있는 거죠
첫 단계로, 여기에 스캐폴딩을 만들었습니다 이제 패키지 매니페스트에서 이전처럼 플러그인 타겟을 추가합니다 플러그인 프로덕트도 추가하겠습니다
라이브러리 프로덕트와 마찬가지로 이런 식으로 플러그인을 개인뿐만 아니라 패키지 클라이언트도 사용할 수 있습니다
앞에서 얘기한 코드를 쓸 수 있습니다
이제 플러그인을 빌드했으므로 별도의 예제 패키지에서 테스트해봅시다
이를 위해 템플릿에서 새 패키지를 생성해 봅시다
패키지에 로컬라이즈된 문자열을 제공하는 API를 추가합니다
그리고 생성된 테스트에서 사용하도록 추가합니다
예상대로 테스트는 잘 됩니다 API가 'World' 문자열을 반환하니까요 플러그인 패키지에 경로 기반 종속성을 추가해 보겠습니다
라이브러리 타겟에 플러그인도 사용하고요
이제 다시 실행해 봅시다
빌드 로그를 보면 플러그인은 빌드가 시작될 때 실행됩니다 그리고 생성된 파일은 타겟에 추가됩니다 리소스 번들을 빌드하고요 리소스 액세서도 생성됩니다 리소스가 처음부터 타겟에 속했던 것처럼요 이제 리소스 번들을 실제로 사용 하도록 코드를 변경해 보겠습니다
마지막으로, 코드를 변경하고
생성된 번들을 살펴보면
여기에 변경 사항이 반영된 것을 볼 수 있습니다
이제 플러그인에 대한 테스트 베드가 생겼습니다 테스트 스위트를 구체화하고 다른 사람들과 플러그인 패키지를 공유할 수 있습니다 즉, 플러그인으로 개발자 도구를 자동화하고 공유할 수 있습니다 사용자 정의 명령은 일반적 작업을 자동화하는 방법을 제공합니다 빌드 도구은 빌드 프로세스 중에 파일을 생성하는 데 사용할 수 있습니다 들어 주셔서 감사합니다!
-
-
3:40 - GenerateContributors plugin target
// MARK: Plugins .plugin( name: "GenerateContributors", capability: .command( intent: .custom(verb: "regenerate-contributors-list", description: "Generates the CONTRIBUTORS.txt file based on Git logs"), permissions: [ .writeToPackageDirectory(reason: "This command write the new CONTRIBUTORS.txt to the source root.") ] )),
-
5:06 - GenerateContributors plugin implementation
import PackagePlugin import Foundation @main struct GenerateContributors: CommandPlugin { func performCommand( context: PluginContext, arguments: [String] ) async throws { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/git") process.arguments = ["log", "--pretty=format:- %an <%ae>%n"] let outputPipe = Pipe() process.standardOutput = outputPipe try process.run() process.waitUntilExit() let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() let output = String(decoding: outputData, as: UTF8.self) let contributors = Set(output.components(separatedBy: CharacterSet.newlines)).sorted().filter { !$0.isEmpty } try contributors.joined(separator: "\n").write(toFile: "CONTRIBUTORS.txt", atomically: true, encoding: .utf8) } }
-
10:28 - Minimum Deployment Target
platforms: [ .macOS("10.15"), .iOS("12.0"), .tvOS("12.0"), .watchOS("6.0"), ],
-
10:35 - Basic SwiftUI view and preview
import SwiftUI struct ContentView: View { var body: some View { Image("Xcode", bundle: .module) .resizable() .frame(width: 200.0, height: 200.0) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
-
14:56 - AssetConstantsExec executable target
.executableTarget(name: "AssetConstantsExec"),
-
15:03 - AssetConstantsExec implementation
import Foundation let arguments = ProcessInfo().arguments if arguments.count < 3 { print("missing arguments") } let (input, output) = (arguments[1], arguments[2]) struct Contents: Decodable { let images: [Image] } struct Image: Decodable { let filename: String? } var generatedCode = """ import Foundation import SwiftUI """ try FileManager.default.contentsOfDirectory(atPath: input).forEach { dirent in guard dirent.hasSuffix("imageset") else { return } let contentsJsonURL = URL(fileURLWithPath: "\(input)/\(dirent)/Contents.json") let jsonData = try Data(contentsOf: contentsJsonURL) let asset🐱alogContents = try JSONDecoder().decode(Contents.self, from: jsonData) let hasImage = asset🐱alogContents.images.filter { $0.filename != nil }.isEmpty == false if hasImage { let basename = contentsJsonURL.deletingLastPathComponent().deletingPathExtension().lastPathComponent generatedCode.append("public let \(basename) = Image(\"\(basename)\", bundle: .module)\n") } } try generatedCode.write(to: URL(fileURLWithPath: output), atomically: true, encoding: .utf8)
-
15:48 - AssetConstantsExec plugin target
.plugin(name: "AssetConstants", capability: .buildTool(), dependencies: ["AssetConstantsExec"]),
-
16:12 - AssetConstantsExec plugin implementation
guard let target = target as? SourceModuleTarget else { return [] } return try target.sourceFiles(withSuffix: "xcassets").map { asset🐱alog in let base = asset🐱alog.path.stem let input = asset🐱alog.path let output = context.pluginWorkDirectory.appending(["\(base).swift"]) return .buildCommand(displayName: "Generating constants for \(base)", executable: try context.tool(named: "AssetConstantsExec").path, arguments: [input.string, output.string], inputFiles: [input], outputFiles: [output]) }
-
20:19 - GenstringsPlugin target
.plugin(name: "GenstringsPlugin", capability: .buildTool()),
-
20:26 - GenstringsPlugin product
.plugin(name: "GenstringsPlugin", targets: ["GenstringsPlugin"]),
-
20:44 - GenstringsPlugin implementation
guard let target = target as? SourceModuleTarget else { return [] } let resourcesDirectoryPath = context.pluginWorkDirectory .appending(subpath: target.name) .appending(subpath: "Resources") let localizationDirectoryPath = resourcesDirectoryPath .appending(subpath: "Base.lproj") try FileManager.default.createDirectory(atPath: localizationDirectoryPath.string, withIntermediateDirectories: true) let swiftSourceFiles = target.sourceFiles(withSuffix: ".swift") let inputFiles = swiftSourceFiles.map(\.path) return [ .prebuildCommand( displayName: "Generating localized strings from source files", executable: .init("/usr/bin/xcrun"), arguments: [ "genstrings", "-SwiftUI", "-o", localizationDirectoryPath ] + inputFiles, outputFilesDirectory: localizationDirectoryPath ) ]
-
21:10 - Localized string API
import Foundation public func GetLocalizedString() -> String { return NSLocalizedString("World", comment: "A comment about the localizable string") }
-
21:44 - Path-based dependency on GenstringsPlugin
.package(path: "../GenstringsPlugin"),
-
21:52 - Use of GenstringsPlugin in library target
plugins: [ .plugin(name: "GenstringsPlugin", package: "GenstringsPlugin"), ]
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.