Structuring Apps
When you create larger iced apps, you might want to have reusable components and views. For that, there is a common approach that is also used in this showcase from the founder of iced.
In this architecture a view/component is independent of the application and is split into three parts, message, the state and the action.
The state will have a normal view function that returns a iced::Element<Message>
.
The update function will differ a bit. Instead of a Task like our main application, it will return an action enum.
Common variants for this enum can be None
, Task(iced::Task<Message>)
and Submit(String)
. You can think of this action like a message that is sent from the view to the parent application that should handle it.
This kind of architecture will enhance composability.
Initial State
The view should also have a function that creates a new instance of that view.
If some task needs to be done for the initial state of the view (i.e. fetching data), something like (Self, iced::Task<Message>)
should be returned by that function.
The task would be executed, and the data is sent via a message to the view.
To differ between a loaded and not loaded state you can use something like this pattern for your state:
enum State {
NotLoaded,
Loaded {
field1: String,
field2: i32
}
}
Mapping
If you want to compose your UI of subparts that have their own functionality, it makes sense to give them their own messages.
But to use a iced::Element<ViewMessage>
in your app that requires a iced::Element<AppMessage>
you have to map them.
For that, you can simply use iced::Element<ViewMessage>.map(AppMessage::ViewMessage)
.
Note: this is a shortcut for
iced::Element<ViewMessage>.map(|view_message| AppMessage::ViewMessage(view_message))
The .map
function takes a closure that takes the message and converts it into another message.
This often requires having a dedicated message variant on the application level that contains the view message.
Map functions like this are available on Task::map
, Subscription::map
, Element::map
.
Example
The example shows an unfinished joke listing app with a view for creating jokes. When the user tries to create a new joke, a default joke, fetched from an API, is provided. The user can get a random joke from the API on a button click as well. All Jokes are listed in the main/default view.
This is how the view for new jokes looks like using this design:
fn main() {
iced::run("Project Structure Example", App::update, App::view).unwrap();
}
#[derive(Debug, Clone)]
enum Message {
// This message is used to handle the new views message
NewMessage(new::Message),
New,
}
#[derive(Debug, Default)]
enum View {
#[default]
Default,
Edit(new::NewView),
}
#[derive(Debug, Default)]
struct App {
view: View,
items: Vec<String>,
}
impl App {
fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::NewMessage(view_message) => {
if let View::Edit(edit) = &mut self.view {
// Call the update method of the edit view
// and handle the returned action
match edit.update(view_message) {
new::Action::None => {}
// If the action is a task, map the
// task to a message, the higher level message
new::Action::Task(task) => return task.map(Message::NewMessage),
new::Action::Submitted(content) => {
self.view = View::Default;
self.items.push(content);
}
}
}
}
Message::New => {
// Create a new view
let (view, task) = new::NewView::new();
self.view = View::Edit(view);
// Run the task and map it to the higher level message
return task.map(Message::NewMessage);
}
}
iced::Task::none()
}
fn view(&self) -> iced::Element<Message> {
match &self.view {
View::Default => {
let items = self
.items
.iter()
.map(|item| iced::widget::text(item).into())
.collect();
iced::widget::column![
iced::widget::button("Edit").on_press(Message::New),
iced::widget::Column::from_vec(items)
]
.into()
}
// If the view is an edit view, call the view method of the edit view
// and map the returned message to the higher level message
View::Edit(edit) => edit.view().map(Message::NewMessage),
}
}
}
mod new {
pub enum Action {
None,
Task(iced::Task<Message>),
Submitted(String),
}
#[derive(Debug, Clone)]
pub enum Message {
Submit,
ChangeContent(String),
RandomJoke,
}
#[derive(Debug, Default)]
pub struct NewView {
content: String,
}
impl NewView {
pub fn new() -> (Self, iced::Task<Message>) {
(Self::default(), Self::random_joke_task())
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::Submit => Action::Submitted(self.content.clone()),
Message::ChangeContent(content) => {
self.content = content;
Action::None
}
Message::RandomJoke => Action::Task(Self::random_joke_task()),
}
}
pub fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text_input("Content", &self.content).on_input(Message::ChangeContent),
iced::widget::button("Random Joke").on_press(Message::RandomJoke),
iced::widget::button("Submit").on_press(Message::Submit)
]
.into()
}
fn random_joke_task() -> iced::Task<Message> {
iced::Task::future(async {
// Fetch a joke from the internet
let client = reqwest::Client::new();
let response: serde_json::Value = client
.get("https://icanhazdadjoke.com")
.header("Accept", "application/json")
.send()
.await
.unwrap()
.json()
.await
.unwrap();
// Parse the response
let joke = response["joke"].as_str().unwrap();
// Return the joke as a message
Message::ChangeContent(joke.to_owned())
})
}
}
}
As you see, the update function produces this action, that the parent of the view can handle:
fn main() {
iced::run("Project Structure Example", App::update, App::view).unwrap();
}
#[derive(Debug, Clone)]
enum Message {
// This message is used to handle the new views message
NewMessage(new::Message),
New,
}
#[derive(Debug, Default)]
enum View {
#[default]
Default,
Edit(new::NewView),
}
#[derive(Debug, Default)]
struct App {
view: View,
items: Vec<String>,
}
impl App {
fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::NewMessage(view_message) => {
if let View::Edit(edit) = &mut self.view {
// Call the update method of the edit view
// and handle the returned action
match edit.update(view_message) {
new::Action::None => {}
// If the action is a task, map the
// task to a message, the higher level message
new::Action::Task(task) => return task.map(Message::NewMessage),
new::Action::Submitted(content) => {
self.view = View::Default;
self.items.push(content);
}
}
}
}
Message::New => {
// Create a new view
let (view, task) = new::NewView::new();
self.view = View::Edit(view);
// Run the task and map it to the higher level message
return task.map(Message::NewMessage);
}
}
iced::Task::none()
}
fn view(&self) -> iced::Element<Message> {
match &self.view {
View::Default => {
let items = self
.items
.iter()
.map(|item| iced::widget::text(item).into())
.collect();
iced::widget::column![
iced::widget::button("Edit").on_press(Message::New),
iced::widget::Column::from_vec(items)
]
.into()
}
// If the view is an edit view, call the view method of the edit view
// and map the returned message to the higher level message
View::Edit(edit) => edit.view().map(Message::NewMessage),
}
}
}
mod new {
pub enum Action {
None,
Task(iced::Task<Message>),
Submitted(String),
}
#[derive(Debug, Clone)]
pub enum Message {
Submit,
ChangeContent(String),
RandomJoke,
}
#[derive(Debug, Default)]
pub struct NewView {
content: String,
}
impl NewView {
pub fn new() -> (Self, iced::Task<Message>) {
(Self::default(), Self::random_joke_task())
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::Submit => Action::Submitted(self.content.clone()),
Message::ChangeContent(content) => {
self.content = content;
Action::None
}
Message::RandomJoke => Action::Task(Self::random_joke_task()),
}
}
pub fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text_input("Content", &self.content).on_input(Message::ChangeContent),
iced::widget::button("Random Joke").on_press(Message::RandomJoke),
iced::widget::button("Submit").on_press(Message::Submit)
]
.into()
}
fn random_joke_task() -> iced::Task<Message> {
iced::Task::future(async {
// Fetch a joke from the internet
let client = reqwest::Client::new();
let response: serde_json::Value = client
.get("https://icanhazdadjoke.com")
.header("Accept", "application/json")
.send()
.await
.unwrap()
.json()
.await
.unwrap();
// Parse the response
let joke = response["joke"].as_str().unwrap();
// Return the joke as a message
Message::ChangeContent(joke.to_owned())
})
}
}
}
The application that would host the view could look like this:
fn main() {
iced::run("Project Structure Example", App::update, App::view).unwrap();
}
#[derive(Debug, Clone)]
enum Message {
// This message is used to handle the new views message
NewMessage(new::Message),
New,
}
#[derive(Debug, Default)]
enum View {
#[default]
Default,
Edit(new::NewView),
}
#[derive(Debug, Default)]
struct App {
view: View,
items: Vec<String>,
}
impl App {
fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::NewMessage(view_message) => {
if let View::Edit(edit) = &mut self.view {
// Call the update method of the edit view
// and handle the returned action
match edit.update(view_message) {
new::Action::None => {}
// If the action is a task, map the
// task to a message, the higher level message
new::Action::Task(task) => return task.map(Message::NewMessage),
new::Action::Submitted(content) => {
self.view = View::Default;
self.items.push(content);
}
}
}
}
Message::New => {
// Create a new view
let (view, task) = new::NewView::new();
self.view = View::Edit(view);
// Run the task and map it to the higher level message
return task.map(Message::NewMessage);
}
}
iced::Task::none()
}
fn view(&self) -> iced::Element<Message> {
match &self.view {
View::Default => {
let items = self
.items
.iter()
.map(|item| iced::widget::text(item).into())
.collect();
iced::widget::column![
iced::widget::button("Edit").on_press(Message::New),
iced::widget::Column::from_vec(items)
]
.into()
}
// If the view is an edit view, call the view method of the edit view
// and map the returned message to the higher level message
View::Edit(edit) => edit.view().map(Message::NewMessage),
}
}
}
mod new {
pub enum Action {
None,
Task(iced::Task<Message>),
Submitted(String),
}
#[derive(Debug, Clone)]
pub enum Message {
Submit,
ChangeContent(String),
RandomJoke,
}
#[derive(Debug, Default)]
pub struct NewView {
content: String,
}
impl NewView {
pub fn new() -> (Self, iced::Task<Message>) {
(Self::default(), Self::random_joke_task())
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::Submit => Action::Submitted(self.content.clone()),
Message::ChangeContent(content) => {
self.content = content;
Action::None
}
Message::RandomJoke => Action::Task(Self::random_joke_task()),
}
}
pub fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text_input("Content", &self.content).on_input(Message::ChangeContent),
iced::widget::button("Random Joke").on_press(Message::RandomJoke),
iced::widget::button("Submit").on_press(Message::Submit)
]
.into()
}
fn random_joke_task() -> iced::Task<Message> {
iced::Task::future(async {
// Fetch a joke from the internet
let client = reqwest::Client::new();
let response: serde_json::Value = client
.get("https://icanhazdadjoke.com")
.header("Accept", "application/json")
.send()
.await
.unwrap()
.json()
.await
.unwrap();
// Parse the response
let joke = response["joke"].as_str().unwrap();
// Return the joke as a message
Message::ChangeContent(joke.to_owned())
})
}
}
}