Доброго времени суток! С вами Анна Жаркова, ведущий разработчик компании Usetech, и мы продолжаем нашу серию статей, посвященных работе с технологией GraphQL при разработке мобильных приложений.
В предыдущих частях мы говорили о подготовке облачного GraphQL бекенда на Hasura и подключении GraphQL API к Android клиенту. Теперь настал черед iOS мобильного приложения.
Для работы нам понадобится библиотека Apollo GraphQL для iOS:
www.apollographql.com/docs/ios
github.com/apollographql/apollo-ios
Наше приложение абсолютно аналогичное Android и включает в себя такие же по функционалу экраны:
— вход
— регистрация
— лента постов
— экран создания и редактирования поста
— экран с информацией о текущем пользователе.
Начнем с установки Apollo GraphQL. Данная библиотека доступна для установки через SPM с github github.com/apollographql/apollo-ios
Достаточно будет выбрать Apollo и ApolloWebSocket библиотеки:
Последняя актуальная версия пакетов 0.49.1:
Также нам понадобится выкачать схему. Для начала поставим через npm apollo-codegen:
npm i apollo-codegen
Теперь запустим выгрузку, указав путь к API, путь для итогового файла и дополнительные заголовки:
apollo-codegen download-schema "<host>.hasura.app/v1/graphql"
--output schema.json --header "x-hasura-admin-secret: <key>"
Полученный schema.json добавим на верхний уровень нашего проекта:
Теперь нам нужно добавить конфигурацию. Идем в настройки нашего проекта на вкладку Build Phase и добавляем скрипт для исполнения через New Run Script Phase:
Назовем эту фазу, например, CLI и перетащим на 2ю строку, чтобы она шла сразу после зависимостей.
Заменим текстовую заглушку следующим контентом:
# Go to the build root and search up the chain to find the Derived Data Path where the source packages are checked out.
DERIVED_DATA_CANDIDATE="${BUILD_ROOT}"
while ! [ -d "${DERIVED_DATA_CANDIDATE}/SourcePackages" ]; do
if [ "${DERIVED_DATA_CANDIDATE}" = / ]; then
echo >&2 "error: Unable to locate SourcePackages directory from BUILD_ROOT: '${BUILD_ROOT}'"
exit 1
fi
DERIVED_DATA_CANDIDATE="$(dirname "${DERIVED_DATA_CANDIDATE}")"
done
# Grab a reference to the directory where scripts are checked out
SCRIPT_PATH="${DERIVED_DATA_CANDIDATE}/SourcePackages/checkouts/apollo-ios/scripts"
if [ -z "${SCRIPT_PATH}" ]; then
echo >&2 "error: Couldn't find the CLI script in your checked out SPM packages; make sure to add the framework to your project."
exit 1
fi
cd "${SRCROOT}/${TARGET_NAME}"
"${SCRIPT_PATH}"/run-bundled-codegen.sh codegen:generate
--target=swift --includes=./**/*.graphql --localSchemaFile="schema.json" API.swift
В скрипте указываем путь к схеме и файлу API, который будет генерироваться на ее основе.
Попробуем сбилдить наш проект. Первая сборка выдаст нам ошибку компиляции. Это потому, что мы не добавили скрипты запросов graphql для генерации нашего API.
Добавим файл с расширением .graphql на тот же уровень, где у нас лежит схема. После компиляции мы получим файл API.swift, который нужно подключить на тот же уровень, где у нас лежат запросы.
Рассмотрим код наших query и mutation:
query PostsQuery {
posts {
post_id, post_text, user_id, user_name, likes, date, image_link
}
}
mutation AddPostMutation($postId: uuid, $text: String, $image: String, $user: String, $userId: uuid, $date: date) {
insert_posts_one(object: {date: $date, image_link: $image, post_id: $postId, post_text: $text, user_id: $userId, user_name: $user}) {
... Post
}
}
mutation DeletePost($postId: uuid!) {
delete_posts_by_pk(post_id: $postId){
post_id
}
delete_comments(where: {post_id: {_eq: $postId}}) {
returning {
comment_id
}
}
}
query Users {
users {
user_email, user_id, user_name
}
}
query GetPostQuery($postId: uuid) {
posts(where: {post_id: {_eq: $postId}}) {
post_id, post_text, user_id, user_name, likes, date, image_link
}
likes(where: {post_id: {_eq: $postId}}){
post_id, user_id
}
comments(where: {post_id: {_eq: $postId}}){
comment_id, comment_text, user_id, post_id, user_name
}
}
mutation CreateUserMutation($name: String, $id: uuid, $email: String, $password: String) {
insert_users_one(object: {user_email: $email, user_id: $id, user_name: $name, password: $password}) {
... User
}
}
query GetUser($email: String, $password: String) {
users(where: {password: {_eq: $password}, user_email: {_eq: $email}}) {
user_email, user_id, user_name
}
}
query Likes($postId: uuid) {
likes(where: {post_id: {_eq: $postId}}){
post_id, user_id
}
}
query Comments($commentId: uuid) {
comments(where: {post_id: {_eq: $commentId}}) {
comment_id, comment_text, user_id, post_id, user_name
}
}
mutation ChangeLikeMutation($postId: uuid, $likes: String) {
update_posts(where: {post_id: {_eq: $postId}} _set: {likes: $likes}) {
__typename
}
}
mutation ChangePostMutation($postId: uuid!, $postText: String, $imageLink: String) {
update_posts(where: {post_id: {_eq: $postId}} _set: {post_text: $postText, image_link: $imageLink}) {
__typename
}
}
mutation CreateComment($postId: uuid, $commentText: String, $id: uuid, $userId: uuid, $userName: String) {
insert_comments_one(object: {post_id: $postId, comment_text: $commentText, comment_id: $id, user_id: $userId, user_name: $userName }) {
... Comment
}
}
fragment User on users {
user_email, user_id, user_name, likes
}
fragment Comment on comments {
comment_id, comment_text, user_id, post_id, user_name
}
fragment Post on posts {
post_id, post_text, user_id, user_name, likes, date, image_link
}
fragment LikeForPost on likes {
post_id, user_id
}
В принципе наши запросы аналогичны тем, что мы использовали для Android приложения. За исключением того, что для Query мы не используем фрагменты. Только для Mutation.
Попробуем сделать тестовый query с фрагментом в ответе:
query GetUserFr($email: String, $password: String) {
users(where: {password: {_eq: $password}, user_email: {_eq: $email}}) {
... User
}
}
Но при компиляции такого запроса мы получим ошибку:
Так же, как в android приложении, у каждого Query и Mutation свой тип из комбинации вложенных классов. Тип, используемый для фрагмента, мы можем безопасно использовать только для Mutation. Фрагмент оперирует не вложенным типом, поэтому для всех Mutation, которые его используют в graphql, это будет считаться одним и тем же типом.
Это автогенерируемый код, поэтому наши исправления здесь бесполезны.
Будем использовать в коде Query явное указание нужных нам полей:
query GetUser($email: String, $password: String) {
users(where: {password: {_eq: $password}, user_email: {_eq: $email}}) {
user_email, user_id, user_name
}
}
Теперь все компилируется.
В отличие от Android все наши типы и поля для работы с GraphQL с маппингом запишутся в один единственный файл:
// Примерное содержание API.swift
import Apollo
import Foundation
public final class PostsQueryQuery: GraphQLQuery {
/// The raw GraphQL definition of this operation.
public let operationDefinition: String =
"""
query PostsQuery {
posts {
__typename
post_id
post_text
user_id
user_name
likes
date
image_link
}
}
"""
public let operationName: String = "PostsQuery"
public init() {
}
public struct Data: GraphQLSelectionSet {
public static let possibleTypes: [String] = ["query_root"]
public static var selections: [GraphQLSelection] {
return [
GraphQLField("posts", type: .nonNull(.list(.nonNull(.object(Post.selections))))),
]
}
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(posts: [Post]) {
self.init(unsafeResultMap: ["__typename": "query_root", "posts": posts.map { (value: Post) -> ResultMap in value.resultMap }])
}
/// fetch data from the table: "posts"
public var posts: [Post] {
get {
return (resultMap["posts"] as! [ResultMap]).map { (value: ResultMap) -> Post in Post(unsafeResultMap: value) }
}
set {
resultMap.updateValue(newValue.map { (value: Post) -> ResultMap in value.resultMap }, forKey: "posts")
}
}
public struct Post: GraphQLSelectionSet {
public static let possibleTypes: [String] = ["posts"]
public static var selections: [GraphQLSelection] {
return [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("post_id", type: .nonNull(.scalar(String.self))),
GraphQLField("post_text", type: .scalar(String.self)),
GraphQLField("user_id", type: .scalar(String.self)),
GraphQLField("user_name", type: .scalar(String.self)),
GraphQLField("likes", type: .scalar(String.self)),
GraphQLField("date", type: .scalar(String.self)),
GraphQLField("image_link", type: .scalar(String.self)),
]
}
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(postId: String, postText: String? = nil, userId: String? = nil, userName: String? = nil, likes: String? = nil, date: String? = nil, imageLink: String? = nil) {
self.init(unsafeResultMap: ["__typename": "posts", "post_id": postId, "post_text": postText, "user_id": userId, "user_name": userName, "likes": likes, "date": date, "image_link": imageLink])
}
Обратите внимание, что при именовании Query и Mutation соответствующие постфиксы добавляются автоматически.
Теперь займемся маппингом полученных типов в наши используемые структуры данных. Для каждого из типов добавим инициализаторы, куда будем передавать параметры типов Query. Для всех Mutation, использующий определенный фрагмент, можно просто передавать один тип.
struct PostItem : Codable, Equatable {
//. . .
init(data: Post) {
self.uuid = data.postId
self.postText = data.postText ?? ""
self.id = data.postId
self.imageLink = data.imageLink ?? ""
self.userId = data.userId ?? ""
self.userName = data.userName ?? ""
self.likeItems = data.likes?.split(separator: ",").map{LikeItem(userId: String($0), postId: data.postId)} ?? [LikeItem]()
self.dateString = data.date
}
init(data: PostsQueryQuery.Data.Post) {
self.postText = data.postText ?? ""
self.id = data.postId
self.uuid = data.postId
self.imageLink = data.imageLink ?? ""
self.userId = data.userId ?? ""
self.userName = data.userName ?? ""
self.likeItems = data.likes?.split(separator: ",").map{LikeItem(userId: String($0), postId: data.postId)} ?? [LikeItem]()
self.dateString = data.date
}
init(data: GetPostQueryQuery.Data.Post) {
self.postText = data.postText ?? ""
self.id = data.postId
self.uuid = data.postId
self.imageLink = data.imageLink ?? ""
self.userId = data.userId ?? ""
self.userName = data.userName ?? ""
self.likeItems = data.likes?.split(separator: ",").map{LikeItem(userId: String($0), postId: data.postId)} ?? [LikeItem]()
self.dateString = data.date
}
}
struct UserData : Codable {
//. . .
init(user: User) {
self.uid = user.userId
self.name = user.userName
self.email = user.userEmail
}
init(user: GetUserQuery.Data.User) {
self.uid = user.userId
self.name = user.userName
self.email = user.userEmail
}
}
struct CommentItem : Codable {
//. . .
init(comment: Comment) {
self.userId = comment.userId
self.postId = comment.postId
self.userName = comment.userName ?? ""
self.text = comment.commentText
self.uuid = comment.commentId
}
init(comment: CommentsQuery.Data.Comment) {
self.userId = comment.userId
self.postId = comment.postId
self.userName = comment.userName ?? ""
self.text = comment.commentText
self.uuid = comment.commentId
}
}
Можно переходить к созданию адаптера для наших типов запросов.
class QueryAdapter {
static let shared = QueryAdapter()
func loginUserQuery(email: String, password: String)-> GetUserQuery {
return GetUserQuery(email: email, password: password)
}
func loginUserQuery(userData: UserData)-> GetUserQuery {
return GetUserQuery(email: userData.email, password: userData.password)
}
func createUser(userData: UserData)->CreateUserMutationMutation {
return CreateUserMutationMutation(name: userData.name,id: UUID().uuidString, email: userData.email, password: userData.password)
}
func createPost(postItem: PostItem)->AddPostMutationMutation {
return AddPostMutationMutation(
postId: UUID().uuidString,
text: postItem.postText,
image: postItem.imageLink , user: postItem.userName,
userId: postItem.userId,
date: "\(Date())")
}
func changeLike(postItem: PostItem)->ChangeLikeMutationMutation {
let likes = postItem.likeItems.map{$0.userId}.joined(separator: ",")
return ChangeLikeMutationMutation(postId: postItem.uuid, likes: likes)
}
func changePost(postItem: PostItem)->ChangePostMutationMutation {
return ChangePostMutationMutation(postId: postItem.uuid, postText: postItem.postText,imageLink: postItem.imageLink)
}
func deletePost(postItem: PostItem)-> DeletePostMutation {
return DeletePostMutation(postId: postItem.uuid)
}
func deletePostBy(postId: String)-> DeletePostMutation {
return DeletePostMutation(postId: postId)
}
func createComment(commentItem: CommentItem)->CreateCommentMutation {
return CreateCommentMutation(postId: commentItem.postId, commentText: commentItem.text, id: UUID().uuidString, userId: commentItem.userId,userName: commentItem.userName)
}
}
Теперь с помощью расширения типов добавим удобный вызов данных методов адаптера:
extension PostItem {
func createPost()->AddPostMutationMutation {
return QueryAdapter.shared.createPost(postItem: self)
}
func changeLike()->ChangeLikeMutationMutation {
return QueryAdapter.shared.changeLike(postItem: self)
}
func changePost()->ChangePostMutationMutation {
return QueryAdapter.shared.changePost(postItem: self)
}
func deletePost()-> DeletePostMutation {
return QueryAdapter.shared.deletePost(postItem: self)
}
func deletePostById()-> DeletePostMutation {
return QueryAdapter.shared.deletePostBy(postId: self.uuid ?? "")
}
}
extension UserData {
func createUser(userData: UserData)->CreateUserMutationMutation {
return QueryAdapter.shared.createUser(userData: self)
}
}
extension CommentItem {
func createComment()->CreateCommentMutation {
return QueryAdapter.shared.createComment(commentItem: self)
}
}
Кажется, мы кое-что забыли. А именно сетевой клиент для запросов.
Согласно документации, для обращений к API без каких-либо заголовок и настроек нам было бы достаточно такого кода:
import Foundation
import Apollo
class Network {
static let shared = Network()
private(set) lazy var apollo = ApolloClient(url: URL(string: "http://host/graphql")!)
}
Но у нас обязательно должен быть заголовок с ключом доступа для Hasura.
Для осуществления запросов с заголовками авторизации наш клиент будет иметь такой вид:
struct NetworkInterceptorProvider: InterceptorProvider {
// These properties will remain the same throughout the life of the `InterceptorProvider`, even though they
// will be handed to different interceptors.
private let store: ApolloStore
private let client: URLSessionClient
init(store: ApolloStore,
client: URLSessionClient) {
self.store = store
self.client = client
}
func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
return [
MaxRetryInterceptor(),
CacheReadInterceptor(store: self.store),
UserManagementInterceptor(),
RequestLoggingInterceptor(),
NetworkFetchInterceptor(client: self.client),
ResponseLoggingInterceptor(),
ResponseCodeInterceptor(),
JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
AutomaticPersistedQueryInterceptor(),
CacheWriteInterceptor(store: self.store)
]
}
}
class HasuraClient {
static let shared = HasuraClient()
private(set) lazy var apollo: ApolloClient = {
// The cache is necessary to set up the store, which we're going to hand to the provider
let cache = InMemoryNormalizedCache()
let store = ApolloStore(cache: cache)
let client = URLSessionClient()
let provider = NetworkInterceptorProvider(store: store, client: client)
let url = URL(string: "https://host.hasura.app/v1/graphql")!
let headers = ["x-hasura-admin-secret": "your key"]
let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider,
endpointURL: url, additionalHeaders: headers)
// Remember to give the store you already created to the client so it
// doesn't create one on its own
return ApolloClient(networkTransport: requestChainTransport,
store: store)
}()
}
Да, нам потребуется по умолчанию очень много интерсептеров, без которых наш код работать не будет. Это сложнее, чем в Android, поэтому оптимальным будет скопировать код интерсептеров из документации, немного адаптировав под нашу задачу:
import Apollo
class UserManagementInterceptor: ApolloInterceptor {
enum UserError: Error {
case noUserLoggedIn
}
private let headers = ["x-hasura-admin-secret": "your key"]
/// Helper function to add the token then move on to the next step
private func addTokenAndProceed<Operation: GraphQLOperation>(
to request: HTTPRequest<Operation>,
chain: RequestChain,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
for header in headers {
request.addHeader(name: header.key, value: header.value)
}
chain.proceedAsync(request: request,
response: response,
completion: completion)
}
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
self.addTokenAndProceed(
to: request,
chain: chain,
response: response,
completion: completion)
}
}
class RequestLoggingInterceptor: ApolloInterceptor {
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
chain.proceedAsync(request: request,
response: response,
completion: completion)
}
}
class ResponseLoggingInterceptor: ApolloInterceptor {
enum ResponseLoggingError: Error {
case notYetReceived
}
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
defer {
// Even if we can't log, we still want to keep going.
chain.proceedAsync(request: request,
response: response,
completion: completion)
}
guard let receivedResponse = response else {
chain.handleErrorAsync(ResponseLoggingError.notYetReceived,
request: request,
response: response,
completion: completion)
return
}
}
}
Подключаем их в нужное место и переходим к работе над запросами.
Напоминаем, что UI и остальная логика у нас готова.
class AuthHelper {
static let shared = AuthHelper()
private let apollo = HasuraClient.shared.apollo
private weak var userStorage = DI.dataContainer.userStorage
var currentUser: UserData? = nil
//MARK: check authorization
func checkAuth()->Bool {
return currentUser != nil || userStorage?.getUser() != nil
}
func isAuthorized()->Bool {
//Load session and check with saved
return currentUser != nil || userStorage?.getUser() != nil
}
//MARK: Login
func login(email: String, password: String, completion: @escaping(Result<UserData,Error>)->Void) {
apollo.fetch(query: QueryAdapter.shared.loginUserQuery(email: email, password: password)) { (result:Result<GraphQLResult<GetUserQuery.Data>,Error>) in
switch result {
case .failure(let error):
print(error)
case .success(let userData):
if let user = userData.data?.users.first {
let u = UserData(user: user)
self.userStorage?.saveUser(data: u)
completion(.success(u))
}
}
}
}
//MARK: registation
func register(name: String, email: String, password: String, completion: @escaping(Result<UserData,Error>)->Void) {
let query = QueryAdapter.shared.createUser(userData: UserData(uid: UUID().uuidString, name: name, email: email, password: password))
apollo.perform(mutation: query){
(result: Result<GraphQLResult<CreateUserMutationMutation.Data>,Error>) in
switch result {
case .failure(let error):
print(error)
completion(.failure( error))
case .success(let data):
if let user = data.data?.insertUsersOne?.fragments.user {
self.currentUser = UserData(user: user)
self.userStorage?.saveUser(data: self.currentUser!)
completion(.success(self.currentUser!))
}
}
}
}
}
Это код, который нам нужен для входа и регистрации.
Мы обращаемся к нашему apollo ApolooClient и с помощью команды fetch для query или perform для Mutation обращаемся к API, передавая в качестве параметра соответствующий тип запроса или изменения, полученный из нашего адаптера.
В качестве результата нам приходит Result<T,Error>, где T – это GraphQLResult<K.Data> с данными Data класса, вложенного в наш Mutation или Query. Внутри Data будет либо фрагмент для Mutation, либо набор полей нашего Query.
Теперь пропишем наши запросы на работу с постами, лайками и комментариями:
class PostService {
private let apollo = HasuraClient.shared.apollo
static let shared = PostService()
private weak var userStorage = DI.dataContainer.userStorage
var currentPosts: [PostItem] = [PostItem]()
//MARK: post
func publishPost(item: PostItem, completion: @escaping(Result<Bool, Error>)->Void) {
guard let user = userStorage?.getUser() else {
return
}
var postItem = item
postItem.userId = user.uid
postItem.userName = user.name
apollo.perform(mutation: QueryAdapter.shared.createPost(postItem: postItem)) {
(result: Result<GraphQLResult<AddPostMutationMutation.Data>,Error>) in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(_):
completion(.success(true))
}
}
}
func updatePost(item: PostItem, completion: @escaping(Result<Bool, Error>)->Void) {
guard let user = userStorage?.getUser(), item.userId == user.uid else {
return
}
let mutation = QueryAdapter.shared.changePost(postItem: item)
apollo.perform(mutation: mutation) {
(result: Result<GraphQLResult<ChangePostMutationMutation.Data>,Error>) in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(_):
completion(.success(true))
}
}
}
func deletePost(postId: String,completion: @escaping(Result<Bool, Error>)->Void) {
let mutation = QueryAdapter.shared.deletePostBy(postId: postId)
apollo.perform(mutation: mutation) {
(result: Result<GraphQLResult<DeletePostMutation.Data>,Error>) in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(_):
completion(.success(true))
}
}
}
//MARK: posts
func loadPosts(completion: @escaping([PostItem])->Void) {
apollo.fetch(query: PostsQueryQuery()){ [weak self]
(result:Result<GraphQLResult<PostsQueryQuery.Data>,Error>) in
guard let self = self else {return}
switch result {
case .failure(let error):
print(error)
case .success(let data):
let posts = data.data?.posts.map{PostItem(data: $0)} ?? [PostItem]()
completion(self.checkLiked(posts: posts))
}
}
}
private func checkLiked(posts: [PostItem])->[PostItem] {
var tempPosts = posts
guard let userId = userStorage?.getUser()?.uid.lowercased() else {
return posts
}
for i in 0..<tempPosts.count {
let postLikes = tempPosts[i].likeItems
tempPosts[i].isLiked = (postLikes .filter{$0.userId.lowercased() == userId}).count > 0
}
return tempPosts
}
func changeLike(postItem: PostItem, completion: @escaping(Result<[PostItem],Error>)->Void) {
guard let userId = userStorage?.getUser()?.uid else {
return
}
var likeItems = postItem.likeItems
if let found = likeItems.filter({$0.userId == userId}).first {
likeItems.remove(item: found)
} else {
likeItems.append(LikeItem(userId: userId, postId: postItem.uuid))
}
let mutation = ChangeLikeMutationMutation(postId: postItem.uuid.lowercased(), likes: likeItems.map{$0.userId}.joined(separator: ","))
apollo.perform(mutation: mutation) {
(result: Result< GraphQLResult<ChangeLikeMutationMutation.Data>,Error>) in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(_):
self.loadPosts { items in
completion(.success(items))
}
}
}
}
//MARK: comments
func publishComment(item: CommentItem, completion: @escaping(Result<Bool, Error>)->Void) {
var comment = item
guard let user = userStorage?.getUser() else {
return
}
comment.userId = user.uid
comment.userName = user.name
let mutation = item.createComment()
apollo.perform(mutation: mutation) {
(result: Result<GraphQLResult<CreateCommentMutation.Data>,Error>) in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(_):
completion(.success(true))
}
}
}
func loadComments(postId: String, completion: @escaping(Result<[CommentItem], Error>)->Void) {
let query = CommentsQuery(commentId: postId)
apollo.fetch(query: query) {
(result: Result<GraphQLResult<CommentsQuery.Data>,Error>) in
switch result {
case .failure(let error):
print(error)
case .success(let data):
let comments = data.data?.comments.map{
CommentItem(comment: $0)
} ?? [CommentItem]()
completion(.success(comments))
}
}
}
}
Получаем готовое приложение:
github.com/anioutkazharkova/graphql_ios_postoram
Подведем итог.
Итак, за 3 статьи мы с вами рассмотрели, как можно сделать небольшое и несложное мобильное приложение с собственным бекендом на GraphQL Hasura. Некоторые моменты работы с данной технологией весьма спорны. Если отвлечься от особенностей реализаций решений Apollo и Hasura, на мой взгляд, самый проблемный момент – это вынужденное и весьма избыточное дублирование типов из-за вложенности в Query или Mutation. В остальном, что выбрать Rest или GraphQL – уже дело вкуса и предпочтений.
Bonus.
Мы с вами использовали Apollo для совершения запросов к нашему API GraphQL У решения есть свои недостатки. Для iOS даже нет еще версии 1.0, и версионность не особенно стабильна.
Мы можем это делать с помощью обычных сетевых запросов. Для этого нам потребуется отправить наш запрос в качестве json body POST. Cделаем специальную структуру Payload для корректного кодирование body.
Например, чтобы получить список постов:
let query = """
query Posts {
posts {
user_name
user_id
post_text
post_id
post_date
likes
image_link
}
}
"""
class NetworkClient {
lazy var urlSession: URLSession? = {
return URLSession(configuration: URLSessionConfiguration.default)
}()
var urlSessionDataTask: URLSessionDataTask? = nil
struct Payload: Encodable {
let query: String
}
private let headers = ["x-hasura-admin-secret": "your key"]
func doQuery() {
guard let url = URL(string: "https://host.hasura.app/v1/graphql") else {
return
}
var urlRequest = URLRequest(url: url)
for header in headers {
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
}
urlRequest.httpMethod = "POST"
urlRequest.httpBody = try? JSONEncoder().encode(Payload(query: query))
let task = self.urlSession?.dataTask(with: urlRequest, completionHandler: { data, response, error in
if let data = data {
let json = String(data: data, encoding: .utf8)
print(json) //Результат получили сюда
}
})
task?.resume()
}
}
Результат нам вернет json c корневым элементом data:
"{
"data": {
"posts": [
{
"user_name": "...",
"user_id": "...",
"post_text": "...",
"post_id": "...",
//...
}
]
}
}"
Можно написать собственный парсер и использовать его без Apollo.
Дерзайте)
www.apollographql.com/docs/ios
github.com/apollographql/apollo-ios
github.com/anioutkazharkova/graphql_ios_postoram