Во время проведения WWDC 2019, одним из самым больших и захватывающих моментом был анонс релиза SwiftUI. SwiftUI — это совершенно новый фреймворк, который позволяет проектировать и разрабатывать пользовательские интерфейсы с написанием меньшего количества кода, декларативным способом.

В отличие от UIKit, который обычно использовался в сочетании с storyboards, SwiftUI полностью основан на программном коде. Тем не менее, синтаксис очень прост для понимания и проект можно быстро просмотреть с помощью Automatic Preview.

Поскольку SwiftUI использует язык Swift, он позволяет создавать приложения той же сложности с гораздо меньшим количеством кода. Более того, использование SwiftUI автоматически позволяет приложению использовать такие функции, как Dynamic Type, Dark Mode, Localization и Accessibility. Кроме того, он доступен на всех платформах, включая macOS, iOS, iPadOS, watchOS и tvOS. Итак, теперь ваш код пользовательского интерфейса может быть синхронизирован на всех платформах, что дает больше времени для того, чтобы сосредоточиться на второстепенном платформо-зависимом коде.

Об этой статье


Важно, чтобы разработчики узнали, как использовать SwiftUI на более ранних стадиях, поскольку компания Apple в конечном итоге сфокусирует большую часть своего внимания на этом фреймворке. В этой статье мы рассмотрим основы SwiftUI и узнаем, как создавать минимальную навигацию, отображать изображения, текст и списки, посредством создания простого списка контактов, который отобразит всех участников нашей команды. При выборе участника команды, приложение отображает детальную информацию, которая содержит изображение пользователя с его краткой биографией. Давайте начнем!

Запустим Xcode 11. На время написания данной статьи, Xcode 11 все еще находится в бета-версии, поэтому некоторые функции могут работать не так, как ожидалось. В этой статье мы будем использовать Swift 5. Несмотря на то, что продвинутые знания языка Swift не обязательны для данной статьи, все же рекомендуется понимание основ языка.

Примечание редактора: Для предварительного просмотра и взаимодействия с изображениями из Canvas в Xcode, убедитесь, что на Mac установлена MacOS версий 10.15 beta.

Создание нового проекта с использованием SwiftUI


Давайте начнем все сначала, чтобы вы могли сразу увидеть, как запустить приложение SwiftUI. Сначала откройте Xcode и выберите пункт «Create new Xcode project». Для платформы iOS выберите Single View App. Введите название приложению и заполните текстовые поля. Однако следует убедиться, что внизу установлен флажок Use SwiftUI. Если вы не выберите эту опцию, Xcode создаст для вас storyboard файл.

image

Xcode автоматически создаст для вас файл с именем ContentView.swift, и удивительным будет то, что предварительный просмотр вашего кода, будет отображен с правой стороны, как показано ниже.

image

Если вы не видите предварительного просмотра, необходимо нажать кнопку Resume в зоне предварительного просмотра. Компиляция проекта займет некоторое время. Будьте терпеливы и дождитесь завершения компиляции.

Теперь давайте посмотрим, как можно изменить эти файлы для создания приложения.

Создание представления в виде списка


Создание представления в виде списка осуществляется в три этапа. Первый — это создание строк в списке. Возможно дизайн похож на UITableView. Для этого необходимо создать ContactRow. Второй этап это передача необходимых данных в список. У меня есть данные, которые уже закодированы, и требуется всего лишь несколько изменений, чтобы связать список с данными. Последний этап — это просто добавление Navigation Bar и встраивание списка в Navigation View. Это довольно просто. Теперь посмотрим, как все это было реализовано в SwiftUI.

Создание списка преподавателей


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

Как мы видим, в сгенерированном коде имеется компонент Text со значением «Hello World». В редакторе кода изменим значение кода на «Simon Ng».

struct ContentView: View {
    var body: some View {
        Text("Simon Ng")
    }
}


Если все работает верно, справа вы должны увидеть автоматическое обновление. Это эффект мгновенного просмотра, что мы и ожидали.

image

Давайте добавим в приложении новый элемент Text. Это будет краткое описание участника. Чтобы в приложении добавить новый элемент интерфейса, необходимо нажать кнопку + в правом верхнем углу. Появится новое окно со списком различных вью. Переместим вью с названием Text и поместим его под первоначальный элемент Text, как показано ниже.

image

Обратите внимание на код слева:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Simon Ng")
            Text("Placeholder")
        }
    }
}


Можно заметить, что новый элемент Text был добавлен под Text вью с значением Simon Ng. Отличие состоит в том, что теперь это вью, похоже, обернул представление в нечто, называемое VStack. VStack используется для вертикального стека, и он является заменой Auto Layout в SwiftUI. Если у Вас имеется опыт разработки программного обеспечения для watchOS, вы вероятно знаете, что здесь нет никаких ограничений, более того все элементы помещаются в группы. При вертикальном стеке все вью будут расположены вертикально.

Теперь измените текст «Placeholder» на «Founder of AppCoda»

Далее, давайте добавим изображение слева от этого текста. Так как мы хотим расположить представление горизонтально к существующим представлениям, то имеется необходимость обернуть VStack в HStack. Для этого, выполним ?+Click на VStack, а затем выберем Embed in HStack. Посмотрим на это ниже:

image

Данный код должен выглядеть следующим образом:

struct ContentView: View {
    var body: some View {
        HStack {
            VStack {
                Text("Simon Ng")
                Text("Founder of AppCoda")
            }
        }
    }
}


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

struct ContentView: View {
    var body: some View {
        HStack {
            Image(systemName: "photo")
            VStack {
                Text("Simon Ng")
                Text("Founder of AppCoda")
            }
        }
    }
}


Начиная с iOS 13, Apple представляет новую функцию под названием SFSymbols. SF Symbols, разработанный компанией Apple, представляет собой набор из более чем 1500 символов, которые можно использовать в приложениях. Поскольку они могут легко интегрироваться с системным шрифтом San Francisco, символы автоматически обеспечивают оптическое вертикальное выравнивание с текстом любого размера. Поскольку у нас пока нет изображений наших преподавателей, будем использовать так называемый placeholder.

image

Теперь сосредоточимся на некоторых незначительных проблемах дизайна. Поскольку имеется необходимость эмуляции внешнего вида UITableRow, давайте выровняем текст по левому краю (т. е. сделаем его главным). Для этого выполним ?+Click на VStack и нажмем Inspect. Выберем значок выравнивания по левому краю, как показано ниже:

image

Далее увидим изменение в коде. Также код будет изменен в реальном времени для отображения новых изменений.

VStack(alignment: .leading) {
    ...
}


Теперь, когда второе текстовое представление является заголовком, давайте изменим шрифт. Как и раньше, ?+Click на текстовом представлении «Founder of AppCoda» в режиме предварительного просмотра и выбираем Inspect. Изменим шрифт на «Subheadline» и отобразим предварительный просмотр и изменение кода в реальном времени.

image

Давайте также изменим цвет и установим его на «Серый». Данный код должен выглядеть следующим образом:

struct ContentView: View {
    var body: some View {
        HStack {
            Image(systemName: "photo")
            VStack(alignment: .leading) {
                Text("Simon Ng")
                Text("Founder of AppCoda")
                    .font(.subheadline)
                    .color(.gray)
            }
        }
    }
}


Теперь, после окончания проектирования ряда сэмплов, мы подошли к волшебной части. Посмотрите, как легко создать список. Выполним ?+Click на HStack и выполним клик на Embed in List. Вуаля! Посмотрите, как код будет автоматически меняться, и пустая область будет отображать 5 красивых новых строк, каждая из которых показывает Simon Ng в качестве члена команды.

image

Также обязательно обратите внимание, как был создан List в коде. Удалив HStack и заменив его повторяющимся List, было создано табличное представление. Теперь подумайте сколько времени вы сэкономили и на сколько меньше кода написали, избегая все эти UITableViewDataSource, UITableViewDelegate, Auto Layout, реализации для Dark Mode и т. д. Все это само по себе показывает мощь и силу SwiftUI. Тем не менее, мы далеки от завершения. Давайте добавим некоторые реальные данные в новый список.

Подключение данных к списку


Данные, которые нам необходимы, это список участников команды и их биография вместе с папкой со всеми их изображениями. Вы можете скачать необходимые файлы здесь. Вы должны найти 2 файла с именами Tutor.swift и Tutor.xcassets.

После загрузки импортируйте файл с расширением Swift и папку ресурсов в проект Xcode. Чтобы их импортировать, просто перетащите их в навигатор проекта.

В файле Tutor.swift объявляем структура Tutor и приводим ее в соответствие с протоколом Identifiable. Вы поймете, почему это важно позже. Также определяем переменные id, name, headline, bio и imageName. Наконец, добавим некоторые тестовые данные, которые будут использоваться в нашем приложении. В Tutor.xcassets имеются изображения всех участников команды.

Вернитесь к ContentView.swift и измените код следующим образом:

struct ContentView: View {
    //1
    var tutors: [Tutor] = []
 
    var body: some View {
        List(0..<5) { item in
            Image(systemName: "photo")
            VStack(alignment: .leading) {
                Text("Simon Ng")
                Text("Founder of AppCoda")
                    .font(.subheadline)
                    .color(.gray)
            }
        }
    }
}
 
#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        //2
        ContentView(tutors: testData)
    }
}
#endif


Все довольно просто:
  1. Определим новую переменную с именем tutors, которая является пустым массивом структур Tutor.
  2. Поскольку мы определяем новую переменную для структуры ContentView, следовательно необходимо также изменить ContentView_Previews, чтобы отобразить это изменение. Установим в testData параметр tutors.

В предварительном просмотре не будет никаких изменений, потому что мы еще не используем тестовые данные. Чтобы отобразить тестовые данные, измените код следующим образом:

struct ContentView: View {
    var tutors: [Tutor] = []
 
    var body: some View {
        List(tutors) { tutor in
            Image(tutor.imageName)
            VStack(alignment: .leading) {
                Text(tutor.name)
                Text(tutor.headline)
                    .font(.subheadline)
                    .color(.gray)
            }
        }
    }
}


Убедимся, что ContentView использует tutors для отображения данных на экране.

Вот так! Посмотрите, как изменилось представление.

image

Изображения отображаются в виде квадрата. Хотелось бы, чтобы они выглядели более округлыми. Давайте посмотрим, как мы можем сделать изображение с закругленными углами. В правом верхнем углу нажмите кнопку + и перейдите на вторую вкладку. Так будет отображен список модификаторов макета, которые вы можете добавлять к изображениям.

image

Ищите «Corner Radius/Угловой радиус», перетащите его из окна предварительного просмотра и поместите его на изображение. Вы должны увидеть измененный код, и изображения предварительного просмотра будет изменено на следующее.

image

Тем не менее, радиус закругления в 3 слишком мал. Итак, измените его на 40. Таким образом, получаем красивые скругленные картинки.

image

Ячейка и список готовы! Далее необходимо отображать детальную информацию, когда пользователь нажимает на ячейку. Давайте начнем работу с создания навигаций.

Создание навигации


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

В SwiftUI обернуть List вью в NavigationView также очень просто. Все, что вам нужно сделать, это изменить код следующим образом:

...
var body : some View {
    NavigationView {
        List(tutors) { tutor in 
            ...
        }
    }
}
...


Необходимо выполнить оборачивание кода List в NavigationView. По умолчанию панель навигации не имеет заголовка. Предварительный просмотр должен переместить список вниз, оставляя очень большой разрыв в середине. Это потому, что мы не установили заголовок для панели навигации. Чтобы исправить это, необходимо установить заголовок, добавив следующую строку кода (т.е. .navigationBarTitle ):

...
var body : some View {
    NavigationView {
        List(tutors) { tutor in 
            ...
        }
        .navigationBarTitle(Text("Tutors"))
    }
}
...


Теперь экран должен выглядеть примерно так:

image

Далее установим кнопку навигации. NavigationButton ведет на новый экран, который находится в стеке навигации. Подобно тому, как мы обернули List в NavigationView, необходимо обернуть содержимое List с помощью NavigationButton, как показано ниже:

...
var body : some View {
    NavigationView {
        List(tutors) { tutor in 
            NavigationButton(destination: Text(tutor.name)) {
                Image(tutor.imageName)
                VStack(alignment: .leading) {
                    Text(tutor.name)
                    Text(tutor.headline)
                        .font(.subheadline)
                        .color(.gray)
                }
            }
        }
        .navigationBarTitle(Text("Tutors"))
    }
}
...


Теперь имя участника команды отображено в подробном представлении. Сейчас самое время проверить это.

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

image

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

image

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

image

Прежде чем перейти к реализации детализированного представления, позвольте мне показать вам хитрый прием, который поможет сделать ваш код более разборчивым. ?+Click NavigationButton и выберите «Extract Subview».

Бум! Вы можете видеть, что весь код в NavigationButton был создан в совершенно новой структуре, которая делает его очень разборчивым. Переименуйте ExtractedView в TutorCell.

Теперь можно получить ошибку в TutorCell. Это потому, что у нас нет параметра tutor для передачи в эту структуру. Исправить ошибку очень просто. Добавьте новую константу в структуру TutorCell следующим образом:

struct TutorCell: View {
    let tutor: Tutor
    var body: some View {
        ...
    }
}


А, в ContentView, добавьте отсутствующий параметр изменив строку на:

...
List(tutors) { tutor in 
    TutorCell(tutor: tutor)
}.navigationBarTitle(Text("Tutors"))
...


Вот и все! Имеется список и ячейки, все они хорошо продуманные и разложены в требуемом порядке! Далее мы собираемся создать детальный вид, который будет показывать всю информацию о преподавателе.

image

Создание представления для отображения детальной информаций.


Давайте создадим новый файл, перейдя в File > New > File. Под iOS выберите SwiftUI View и назовите этот файл TutorDetail.

image

В предварительном просмотре уже создался главный базовый вид. Давайте с ним поработаем. Сначала нажмите на кнопку « +» и поместите изображение над уже встроенным представлением Text. Установите имя изображения «Simon Ng». Должно появится изображение Саймона. Теперь измените код так, как показано ниже:

struct TutorDetail: View {
    var body: some View {
        //1
        VStack {
            //2
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            //3
            Text("Simon Ng")
                .font(.title)
        }
    }
}


В целом этот код достаточно понятен, но в случае если у Вам нужны разъяснения, не волнуйтесь. Вот, что происходит:
  1. Сначала мы упаковываем все наши представления в вертикальный стек. Это имеет решающее значение для макета дизайна, который мы будем принимать.
  2. Затем берем изображение Саймона и оживляем его. Сначала установим клипы изображения в форме круга. Вместо того, чтобы установить cornerRadius, это намного эффективнее, поскольку круг можно приспособить к различным размерам изображения. Мы добавляем наложение круга с белой рамкой, которая обеспечивает красивую оранжевую рамку. Наконец, мы добавим легкую тень, чтобы обеспечить некоторую глубину изображения.
  3. Наша последняя строка кода устанавливает шрифт имени преподавателя на шрифт заголовка.


image

Также необходимо добавить еще два текстовых вью: headline и bio. Перетащите два текстовых вью ниже текстового вью с именем преподавателя, и отредактируйте их:

struct TutorDetail: View {
    var body: some View {
        VStack {
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            Text("Simon Ng")
                .font(.title)
            Text("Founder of AppCoda")
            Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
        }
    }
}


image

Хорошей новостью является то, что у нас есть текстовые вью. Плохая новость в том, что они плохо выглядят и не показывают разницу между заголовком и детальным описанием. Кроме того, текстовое вью биографии не отображает весь текст. Давайте исправим это.

Обновите код следующим образом:

struct TutorDetail: View {    
    var body: some View {
        VStack {
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            Text("Simon Ng")
                .font(.title)
            //1
            Text("Founder of AppCoda")
                .font(.subheadline)
            //2
            Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
                .font(.headline)
                .multilineTextAlignment(.center)
 
        }
    }
}


  1. Сначала мы устанавливаем “Founder of AppCoda” со шрифтом subheadline.
  2. Точно так же мы устанавливаем текстовое представление биографии используя шрифт headline. Мы также выровняем текст с линией .multilineTextAlignment(.center)


image

Давайте исправим следующую ошибку. Нам необходимо отобразить весь текст текстового представления биографии. Это можно легко сделать, добавив новую строку кода:

...
Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
        .font(.headline)
        .multilineTextAlignment(.center)
        .lineLimit(50)
...


image

Все выглядит хорошо. Есть одно последнее изменение дизайна, которое я хочу сделать. Заголовок и текстовые представления биографии находятся слишком близко друг к другу. Я хотел бы иметь некоторое пространство между этими двумя представлениями. Кроме того, я хотел бы добавить некоторые отступы ко всем вью, чтобы они не касались краев устройства. Убедитесь, что вы изменили код следующим образом:

struct TutorDetail: View {
    var body: some View {
        VStack {
            Image("Simon Ng")
                 .clipShape(Circle())
                .overlay(
                    Circle().stroke(Color.orange, lineWidth: 4)
                )
                .shadow(radius: 10)
            Text("Simon Ng")
                .font(.title)
            Text("Founder of AppCoda")
                .font(.subheadline)
            //1
            Divider()
 
            Text("Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
                .font(.headline)
                .multilineTextAlignment(.center)
                .lineLimit(50)
        //2
        }.padding()
    }
}


Здесь выполним несколько изменений:
  1. Добавить разделитель так же просто, как и вызвать Divider()
  2. Чтобы добавить отступы ко всему вертикальному стеку, необходимо вызвать .padding() в конце объявления VStack.


image

Это все! Поздравляю! Экран детального просмотра готов. Осталось только соединить наш список преподавателей и их детальное описание. Это довольно просто.

Передача данных


Для передачи данных необходимо объявить некоторые параметры в структуре TutorDetail. Перед объявлением переменной body добавьте следующие переменные:

var name: String
var headline: String
var bio: String
var body: some View {
    ...
}


Это параметры, которые мы передадим из ContentView. Проведите следующие изменения:

...
var body: some View {
    VStack {
        // 1
        Image(name)
            .clipShape(Circle())
               .overlay(
                   Circle().stroke(Color.orange, lineWidth: 4)
            )
               .shadow(radius: 10)
        //2
        Text(name)
            .font(.title)
        //3
        Text(headline)
            .font(.subheadline)
        Divider()
        //4
        Text(bio)
            .font(.headline)
            .multilineTextAlignment(.center)
            .lineLimit(50)
        //5
    }.padding().navigationBarTitle(Text(name), displayMode: .inline)
}
...


  1. Заменим имя преподавателя для image на переменную name
  2. Заменим текст заголовка на переменную headline
  3. Наконец, заменим длинный абзац текста на переменную bio
  4. Также была добавлена строка кода, которая установит заголовок панели навигации на имя преподавателя.


И последнее, но не менее важное: нам необходимо добавить отсутствующие параметры в структуру TutorDetail_Previews.

#if DEBUG
struct TutorDetail_Previews : PreviewProvider {
    static var previews: some View {
        TutorDetail(name: "Simon Ng", headline: "Founder of AppCoda", bio: "Founder of AppCoda. Author of multiple iOS programming books including Beginning iOS 12 Programming with Swift and Intermediate iOS 12 Programming with Swift. iOS Developer and Blogger.")
    }
}    
#endif


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

Вы можете быть удивлены, что случилось с инструкциями #if DEBUG/#endif. Это означает, что любой код, заключенный в эти команды, будет выполнен только при предварительном просмотре для целей отладки. В вашем последнем приложении этого не будет.

Ничто не должно измениться, так как информация также неизменна.

image

И так, последний шаг — связать это представление со списком. Переключитесь на файл ContentView.swift. Все, что необходимо сделать, это изменить одну строку кода в структуре TutorCell. Измените код NavigationButton на ниже приведенный:

...
var body: some View {
    return NavigationButton(destination: TutorDetail(name: tutor.name, headline: tutor.headline, bio: tutor.bio)) {
        ...
    }
}
...


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

image

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

Просто выберите одну из записей участника:

image

И тогда будут отображены детали участника на детальном экране.

image

Заключение


В этой статье представлены основы SwiftUI. Теперь будет удобно создавать простые приложения, такие как планировщик задач и т.д. Я предлагаю взглянуть на некоторые из приведенных ниже ресурсов, таких как документация от компании Apple и сессии WWDC 2019, посвященные данному фреймворку.

SwiftUI Documentation
SwiftUI Tutorials
Introducing SwiftUI: Building Your First App
SwiftUI Essentials

Этот фреймворк — это будущее развития Apple, поэтому очень здорово, если вы начнете именно с него. Помните, что если вы не уверены в коде, попробуйте поработать с автоматическим предварительным просмотром и посмотрите, сможете ли вы внести изменения в пользовательский интерфейс напрямую, чтобы увидеть, как создается код. Если у вас есть какие-либо вопросы, не стесняйтесь, задавайте их в комментариях ниже.

Для справки вы можете скачать готовый проект здесь.

Комментарии (8)


  1. prs123
    29.07.2019 16:50

    То есть раньше, если дизайнер мог сверстать интерфейс в StoryBoard'ах не погружаясь в код, то теперь программист должен сам верстать интерфейс?


    1. yarmolchuk Автор
      29.07.2019 17:49

      Думаю теперь станет еще легче отдать верстку дизайнеру.


      1. prs123
        29.07.2019 17:54

        Чтобы он учился код писать?


        1. yarmolchuk Автор
          29.07.2019 20:52
          +1

          Можно и не писать код. Можно все мышкой UI сделать…


  1. ShadowMaster
    29.07.2019 21:27

    Может стоит сначала сказать о системных требованиях в macOS 10.15 и iOS 13?


    1. yarmolchuk Автор
      30.07.2019 13:04

      Про это написано в статье, вы наверно пропустили этот момент.


      1. ShadowMaster
        30.07.2019 20:04

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


        1. yarmolchuk Автор
          30.07.2019 23:14

          Если будет время и желание загляните сюда